单例模式与线程保险
请看如下的单例类:
class Singleton{ private static Singleton singleton = null; public static Singleton getSingleton() { if (null == singleton) { singleton = new Singleton(); } return singleton; }
首先判断singleton是否为null,如果是就创建singleton对象,否则直接返回singleton。但是判断和创建并非原子操作,假设线程1正在执行null == singleton,判断为true,准备执行下一句new Singleton();此时线程2可能已经new了一个 Singleton了,线程1再次new了一个Singleton,出现2个Singleton与单例的设计思想不符,即单例的控制在并发情况下失效了,测试代码可直观的反应该问题:
public class MyThread extends Thread { public void run() { System.out.println(Singleton.getSingleton().toString()); } public static void main(String[] args) { for(int i=0;i<10;i++){ MyThread myThread = new MyThread(); myThread.start(); } } } class Singleton{ private static Singleton singleton = null; public static Singleton getSingleton() { if (null == singleton) { singleton = new Singleton(); } return singleton; } }
输出:
Singleton@69b332
Singleton@69b332
Singleton@69b332
Singleton@69b332
Singleton@173a10f
Singleton@69b332
Singleton@69b332
Singleton@69b332
Singleton@69b332
Singleton@69b332
可以在getSingleton方法前加synchronized,确保任意时刻都只有一个线程可以进入该方法。但是这样一来,会降低整个访问的速度,而且每次都要判断。
可以使用"双重检查加锁"的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。
public class Singleton { /** * 对保存实例的变量添加volatile的修饰 */ private volatile static Singleton instance = null; private Singleton(){ } public static Singleton getInstance(){ //先检查实例是否存在,如果不存在才进入下面的同步块 if(instance == null){ //同步块,线程安全地创建实例 synchronized(Singleton.class){ //再次检查实例是否存在,如果不存在才真正地创建实例 if(instance == null){ instance = new Singleton(); } } } return instance; } }
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存 在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来, 就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
双重检查机制,尽可能缩小了同步块的范围。
这里解释一下为什么要判断2次,当两个线程调用getInstance方法时,它们都将通过第一重instance==null的判断,由于同步机制,这2个线程只能有一个进入,另一个排队。而此时如果没有第二重判断,则第一个线程创建了实例,而第二个实例离开队列重新获得锁,则将继续创建实例,这样就没有达到单例的目的。
在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:
由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时
访问 final 字段时
在创建线程之前创建对象时
线程可以看见它将要处理的对象时。
一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。
看看代码示例可能会更清晰一些,示例代码如下:
public class Singleton { /** * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 * 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载 */ private static class SingletonHolder{ /** * 静态初始化器,由JVM来保证线程安全 */ private static Singleton instance = new Singleton(); } /** * 私有化构造方法 */ private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.instance; } }
当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致 SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的 域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本