一 单例模式的使用场景
单例模式用于确保某一个类有且只有一个实例,避免产生多个对象消耗过多的资源。所以当你所要定义的类会消耗较多的资源,例如访问IO和数据库等资源,就要考虑使用单例模式了。举个例子,在Android开发中出色的图片加载框架ImageLoader就使用了单例模式。因为ImageLoader中含有线程池、缓存系统、网络请求等等,比较耗费资源,如果没加载一张图片就去新建一个ImageLoader的实例则非常耗费资源,所以程序的设计者使用了单例模式,以让一个应用程序中有且仅有一个ImageLoader的实例。
二 单例模式的实现方法
实现单例模式主要有以下几个重点:
(1)构造方法不对外开放,一般为private
(2)通过静态方法返回实例
(3)确保类的实例有且仅有一个,特别是在多线程的环境下
(4)尽量确保线程安全
掌握内在思想后,就要开始实践了,单例模式的实现方法多种多样,且各有千秋,下面由简至繁一一举例,并且进行点评。
1、饿汉模式
public class Singleton {
private Singleton() {}
private static final Singleton single = new Singleton();//静态工厂方法
public static Singleton getInstance() {
return single;}
}
上述这种实现方法叫饿汉模式,从代码中可以看到,外届只能通过Singleton.getInstance()方法获得Singleton的实例(single),而这个实例是静态对象,并且在声明的时候就初始化了,这就保证了Sigleton对象的唯一性,以后不再改变,所以天生是线程安全的。
2、懒汉模式
//懒汉式单例类.在第一次调用的时候实例化自己public class Singleton {
private Singleton() {}
private static Singleton single=null;//静态工厂方法
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();}
return single;}
}
懒汉模式同样将构造函数私有化,对外提供Singleton.getInstance()方法获得Singleton的实例(single),不同的是饿汉模式中在类加载的时候就已经初始化了single实例,而懒汉模式中只有开发者调用了Singleton.getInstance()才去创建Single的实例对象(single),同时,细心地读者应该发觉Singleton.getInstance()方法加了一个synchronized 的关键字,这个关键字就是用来保证线程安全的。这种方法实现了单例模式也保证了线程安全,但是每次调用Singleton.getInstance()方法都需要进行同步以保证线程安全造成了很多不必要的同步开销,消耗资源。这个就是懒汉模式的缺点。
3、双重加锁(Double Check Lock)(比较推荐)
public class Singleton {
private Singleton() {}
private volatile static Singleton single=null;//静态工厂方法
public static Singleton getInstance() {
if (single == null) {
synchronized (Singleton.class) {
if (single== null) {
single = new Singleton();}
}
}
return single;}
}
Double Check Lock双重加锁,简称DCL,这种方式实现单例既能够在需要使用时才初始化单例,进行了同步保证了线程安全,也提高了资源的利用率。它的亮点在于Singleton.getInstance()方法中的对single进行了两层判空,第一层判空是为了避免不必要的同步,第二层判空是为了在single为null的情况下才创建实例。读者应该注意到单例对象single 定义添加了关键字volatile ,添加该字段是因为程序执行
single = new Singleton();
这个语句时其实做了三步工作,(1)分别是给Singleton的实例分配内存,(2)调用构造函数初始化成员字段,(3)将single对象指向分配的内存空间(single不再为null),由于Java虚拟机是乱序执行的,所以执行顺序可能是(1)(2)(3),也有可能是(1)(3)(2),在多线程情况下,假如(3)执行完成,(2)未执行,线程从A切换线程B,因为此时single不为null,所以线程B直接取走了未执行步骤(2)的single,因为此时single未调用构造函数初始化,所以B线程在使用single时会出错,此时双重加锁就会失效,上面添加的volatile关键字就是为了解决这个问题,它可以保证single每次都从主存中读取。但是volatile也会影响性能,一般来说在Android开发中,高并发的情况比较少见,所以在实际开发中大多数开发者选择把volatile 字段去掉。
4、静态内部类方式(也不错的)
public class Singleton {
private static class Holder {
private static final Singleton INSTANCE = new Singleton();}
private Singleton (){}
public static final Singleton getInstance() {
return Holder.INSTANCE;}
}
DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是在也会存在失效的可能,严重会导致程序崩溃。在《Java并发编程实践》中也谈到了这个问题,并直指这种优化是丑陋的不赞成使用,而提倡了上面这种静态内部类的实现方式。这种方式比上面1、2、3都好一些,既实现了线程安全,又避免了同步带来的性能影响,保证了单例对象的唯一性,同时延迟了单例的实例化。
仁者见仁智者见智,在Android开发实践中,通过阅读别人的代码发现,大部分开发者偏向于使用第三种双重加锁的实现方式,同时第四种方法得到圣书《Java并发编程实践》的力推,优点也是显而易见,所以个人推荐第三第四种实现方式。