1 概述
单例模式是应用最广泛的模式之一,该模式确保某各类只有一个实例对象,而且自行实例化并向整个系统提供这个实例。
2 使用场景
确保各类有且只有一个对象的场景,避免产生多个对象小豪过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问的IO和数据库等资源,这时就要考虑使用单例模式。
3 UML类图
角色介绍:
- Client::高层客户端
- Singleton:单例类
实现单例模式主要关键点:
- 构造函数不对外开放,一般为private
- 通过一个静态方法或枚举返回单例类对象
- 确保单例类对象在反序列化时不会重新构建对象
通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。
4 单例模式类型
单例模式有以下几种:
- 饿汉单例模式
- 懒汉单例模式
- DCL实现单例模式
- 静态内部类单例模式
- 枚举单例模式
- 使用容器实现单例模式
下面一一具体介绍每个单例模式的实现和优缺点。
4.1 饿汉单例模式
public class Role {
private static final Role sInstance = new Role();
private Role() {}
public static Role getInstance() {
return sInstance;
}
}
由于Role的构造函数私有化,因此Role的实例不能通过new创建,只能通过Role.getInstance()获取,而且该实例是在类加载的时候就已经创建了,因此得名饿汉单例模式,非常形象。那么什么时候类被加载呢?以下三种情况:
- 生成该类对象的时候,会加载该类及该类的所有父类
- 访问该类的静态成员的时候
- class.forName(“类名”)
通过这种方式创建的单例实例的好处是不用担心线程安全的问题,因为在类加载的时候就已经创建了。缺点就是因为还没调用该类就已经创建了,所以如果没调用该类,就会造成资源浪费,但这点资源在如今的硬件中可忽略不计。
4.2 懒汉单例模式
public class Role {
private static Role sInstance = null;
private Role() {}
public static synchronized Role getInstance() {
if(sInstance == null) {
sInstance = new Role();
}
return sInstance;
}
}
该方式是在该类被调用时才创建实例,getInstance()函数添加了synchronized关键字是为了应对线程安全问题,这样就能保证多线程开发中当且仅有一个实例创建,但是一旦该实例创建成功,后续继续调用getInstance()获取实例,都会进行同步,synchronized是一个很耗资源的机制,这样虽然保证了线程安全,但是会造成不必要的同步开销,因此不建议使用该方式创建单例实例。
4.3 DCL实现单例模式
上小节的懒汉单例模式虽然能创建单例实例,并且线程安全,但是多次调用getInstance()会造成资源浪费,DCL的意思是双重检查锁懒汉单例模式,该方式的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance()不进行同步锁,代码如下:
public class Role {
private static Role sInstance = null;
private Role() {}
public static Role getInstance() {
if(sInstance == null) {
synchronized(Role.class) {
if(sInstance == null) {
sInstance = new Role();
}
}
}
return sInstance;
}
}
getInstance()方法中对sInstance单例实例进行了两次判空,目的是:第一层判断是为了避免不必要的同步,第二层的判断是为了在null的情况下创建实例。
但是该种方式创建单例实例在高并发的情况下有可能失效,下面具体来分析下原因。
sInstance = new Role()这个方法实际上并不是一个原子操作,这句代码最终被翻译成多条汇编指令,大概做了3件事情:
- 给Role的实例分配内存;
- 调用Role()的构造函数,初始化成员字段;
- 将sInstance 对象指向分配的内存空间(此刻sInstance 对象就不为null了)
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前的内存模型中Cache、寄存器到主内存回写顺序的规定,上面的步骤2和步骤3就无法保证,即执行顺序有可能是1-2-3,也有可能是1-3-2。如果是后者,线程A在3执行完毕,2未执行的情况下,线程被切换到B,这时候线程B仅限第一层的判空,由于线程A已经执行了步骤3,此刻sInstance已经不为null了,因此线程B直接取走了sInstance,但是sInstance还没调用上述的步骤2,也就是说sInstance对象还没完全创建成功,如果使用就会出错,这就是该方式创建单例失效的原因。
好在JDK1.5或之后的版本已经修复了该问题,方案是用将sInstance定义为volatile,即private volatile static Role sInstance = null就可以保证sInstance 对象每次都是从主内存中读取,就可以解决该模式失效的问题。
DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证对象的唯一性,除非你的代码在并发场景比较复杂或版本低于JDK1.5版本下使用,否则该方式一般能够满足需求。
4.4 静态内部类单例模式
在《Java并发编程实践》该书中,建议使用静态内部类单例模式,代码如下:
public class Role {
private Role() {}
public static Role getInstance() {
return RoleHolder.sInstance;
}
private static class RoleHolder {
private static final Role sInstance = new Role();
}
}
由于类加载时不会执行静态内部类,因此第一次加载Role类时并不会初始化sInstance,只有在第一次调用Role的getInstance()方法时才会导致sInstance被初始化。因此,第一次调用getInstance()方法会导致虚拟机加载RoleHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。
4.5 枚举单例模式
在《Effect Java》 中说道,最佳的单例实现模式就是枚举单例模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
public enum RoleEnum {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
调用方法:
RoleEnum.INSTANCE.doSomething();
这里有几个原因关于为什么在Java中宁愿使用一个枚举量来实现单例模式:
- 自由序列化;
- 保证只有一个实例(即使使用反射机制和反序列化也无法多次实例化一个枚举量);
- 线程安全;
5 Java反射技术破坏单例模式
Java中的反射技术可以获取类的所有方法、成员变量、还能访问private的构造方法,这样一来,单例模式中用的私有构造函数被调用就会产生多个实例,拿静态内部类创建单例模式来测试一下。
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
除了反射攻击之外,还可能存在反序列化攻击的情况。
总的来说,一般情况下没必要考虑反射技术和反序列化攻击破坏单例的,上面几种方式创建单例,通过枚举创建单例是最理想的方式了,枚举特性是JDK1.5引入的,因此,JDK1.5及之后的版本,推荐首选枚举创建单例。
6 Android源码中的单例模式
frameworks/base/core/ava/android/app/ResourcesManager.java
public class ResourcesManager {
private static ResourcesManager sResourcesManager;
...
public static ResourcesManager getInstance() {
synchronized (ResourcesManager.class) {
if (sResourcesManager == null) {
sResourcesManager = new ResourcesManager();
}
return sResourcesManager;
}
}
}
但在该类中没有将其构造函数私有化,也就是说是可以通过new ResourcesManager()的方式来创建ResourcesManager实例的,但是由于ResourcesManager.java是系统的类,而且该类标注了@hide声明,也就是说普通的app是不能访问ResourcesManager.java的,因此,不担心开发者创建多个ResourcesManager实例。但系统层面的模块还是可以通过new ResourcesManager()的方式来创建多个ResourcesManager实例的,只不过可能goole的工程师编写Android源码时约定指定通过ResourcesManager.getInstance()的方式取得ResourcesManager的实例吧。
再举一个Android源码应用单例模式的例子。
frameworks/base/core/java/android/app/ActivityManager.java
frameworks/base/core/java/android/uti/Singleton.java
创建ActivityManager.java实例的方式:
[->ActivityManager.java]
public class ActivityManager {
...
/*package*/ ActivityManager(Context context, Handler handler) {
mContext = context;
}
...
/**
* @hide
*/
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
...
}
创建AMS的远端代理实例IActivityManager,是通过getService()方法创建的,调用Singleton实例方法的get方法取得IActivityManager实例。
事实上类加载时就已经初始化了IActivityManagerSingleton实例,这正是饿汉单例模式的方式。执行new Singleton的create()方法就是通过ServiceManager.getService(Context.ACTIVITY_SERVICE)的方式取得IActivityManager实例,然后调用Singleton的get()方法就可取得创建的IActivityManager实例。
[->Singleton.java]
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
7 总结
由于在客户端通常没有高并发,因此选择哪种实现方式来创建单例模式都并不会有太大的影响,处于效率考虑,推荐使用DCL实现单例模式和静态内部类单例模式。 另外需要注意的是,单例对象如果持有Context,是很容易引起内存泄露的,需要注意传递给单例对象的Context对好是Application Context。