lazy模式:html
其优缺点已经在备注中注明,来重点分析一下非线程安全的部分:java
一、当线程A进入到第28行(#1)时,检查instance是否为空,此时是空的。
二、此时,线程B也进入到28行(#1)。切换到线程B执行。一样检查instance为空,因而往下执行29行(#2),建立了一个实例。接着返回了。
三、在切换回线程A,因为以前检查到instance为空。因此也会执行29行(#2)建立实例。返回。
四、至此,已经有两个实例被建立了,这不是咱们所但愿的。 安全
那么怎么去解决线程安全问题jvm
第一种方法,在上图中的getInstance()方法加上synchronized关键字ide
加上synchronized后确实实现了线程的互斥访问getInstance()方法。从而保证了线程安全。可是这样就完美了么?咱们看。其实在第一种lazy模式实现里,会致使问题的只是当instance尚未被实例化的时候,多个线程访问#1的代码才会致使问题。而当instance已经实例化完成后。每次调用getInstance(),其实都是直接返回的。即便是多个线程访问,也不会出问题。但给方法加上synchronized后。全部getInstance()的调用都要同步了。其实咱们只是在第一次调用的时候要同步。而同步须要消耗性能。这就是问题。性能
方法二:双重检查加锁Double-checked locking。
其实通过分析发现,咱们只要保证 instance = new SingletonOne(); 是线程互斥访问的就能够保证线程安全了。那把同步方法加以改造,只用synchronized块包裹这一句。就获得了下面的代码:spa
这个方法可行么?分析一下发现是不行的!
一、线程A和线程B同时进入//1的位置。这时instance是为空的。
二、线程A进入synchronized块,建立实例,线程B等待。
三、线程A返回,线程B继续进入synchronized块,建立实例。。。
四、这时已经有两个实例建立了。 线程
为了解决这个问题。咱们须要在//2的以前,再加上一次检查instance是否被实例化。(双重检查加锁)接下来,代码变成了这样:code
这样,当线程A返回,线程B进入synchronized块后,会先检查一下instance实例是否被建立,这时实例已经被线程A建立过了。因此线程B不会再建立实例,而是直接返回。貌似!到此为止,这个问题已经被咱们完美的解决了。遗憾的是,事实彻底不是这样!这个方法在单核和 多核的cpu下都不能保证很好的工做。致使这个方法失败的缘由是当前java平台的内存模型。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制致使了双重检查加锁方法的失效。这个问题的关键在上面代码上的第5行:instance = new SingletonThree(); 这行其实作了两个事情:一、调用构造方法,建立了一个实例。二、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法以前,instance已经被设置为非空了。下面咱们看一下出问题的过程:
一、线程A进入getInstance()方法。
二、由于此时instance为空,因此线程A进入synchronized块。
三、线程A执行 instance = new SingletonThree(); 把实例变量instance设置成了非空。(注意,实在调用构造方法以前。)
四、线程A退出,线程B进入。
五、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并非SingletonThree的实例,由于没有调用构造方法。)
六、线程B退出,线程A进入。
七、线程A继续调用构造方法,完成instance的初始化,再返回。 orm
好吧,继续努力,解决由“无序写”带来的问题。
解释一下执行步骤。
一、线程A进入getInstance()方法。
二、由于instance是空的 ,因此线程A进入位置//1的第一个synchronized块。
三、线程A执行位置//2的代码,把instance赋值给本地变量temp。instance为空,因此temp也为空。
四、由于temp为空,因此线程A进入位置//3的第二个synchronized块。
五、线程A执行位置//4的代码,把temp设置成非空,但尚未调用构造方法!(“无序写”问题)
六、线程A阻塞,线程B进入getInstance()方法。
七、由于instance为空,因此线程B试图进入第一个synchronized块。但因为线程A已经在里面了。因此 没法进入。线程B阻塞。
八、线程A激活,继续执行位置//4的代码。调用构造方法。生成实例。
九、将temp的实例引用赋值给instance。退出两个synchronized块。返回实例。
十、线程B激活,进入第一个synchronized块。
十一、线程B执行位置//2的代码,把instance实例赋值给temp本地变量。
十二、线程B判断本地变量temp不为空,因此跳过if块。返回instance实例。
好吧,问题终于解决了,线程安全了。可是咱们的代码由最初的3行代码变成了如今的一大坨~。因而又有了下面的方法。
预先初始化static变量
/** * 预先初始化static变量 的单例模式 非Lazy 线程安全 * 优势: * 一、线程安全 * 缺点: * 一、非懒加载,若是构造的单例很大,构造完又迟迟不使用,会致使资源浪费。 * * @author laichendong * @since 2011-12-5 */ public class SingletonFour { /** 单例变量 ,static的,在类加载时进行初始化一次,保证线程安全 */ private static SingletonFour instance = new SingletonFour(); /** * 私有化的构造方法,保证外部的类不能经过构造器来实例化。 */ private SingletonFour() { } /** * 获取单例对象实例 * * @return 单例对象 */ public static SingletonFour getInstance() { return instance; } }
看到这个方法,世界又变得清净了。因为java的机制,static的成员变量只在类加载的时候初始化一次,且类加载是线程安全的。因此这个方法实现的单例是线程安全的。可是这个方法却牺牲了Lazy的特性。单例类加载的时候就实例化了。如注释所述:非懒加载,若是构造的单例很大,构造完又迟迟不使用,会致使资源浪费。
那到底有没有完美的办法?懒加载,线程安全,代码简单。
使用内部类。
/** * 基于内部类的单例模式 Lazy 线程安全 * 优势: * 一、线程安全 * 二、lazy * 缺点: * 一、待发现 * * @author laichendong * @since 2011-12-5 */ public class SingletonFive { /** * 内部类,用于实现lzay机制 */ private static class SingletonHolder{ /** 单例变量 */ private static SingletonFive instance = new SingletonFive(); } /** * 私有化的构造方法,保证外部的类不能经过构造器来实例化。 */ private SingletonFive() { } /** * 获取单例对象实例 * * @return 单例对象 */ public static SingletonFive getInstance() { return SingletonHolder.instance; } }
解释一下,由于java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),并且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。
最后,总结一下: 一、若是单例对象不大,容许非懒加载,可使用方法三。 二、若是须要懒加载,且容许一部分性能损耗,可使用方法一。(官方说目前高版本的synchronized已经比较快了) 三、若是须要懒加载,且不怕麻烦,可使用方法二。 四、若是须要懒加载,没有且!推荐使用方法四。