单例模式都不陌生,有饿汉式单例、懒汉式单例等。
饿汉式单例:
public class Singleton { PRivate static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; }}在类第一次加载的时候,单例就完成了初始化。下面来验证饿汉式单例的线程安全性:
public class MyThread extends Thread{ public void run() { System.out.println(Singleton.getInstance().hashCode()); }}public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); }}运行得到:
763347431 763347431 763347431
三次得到的 hashcode() 返回值都一样。
结论:饿汉式单例在类第一次加载的时候完成初始化,是线程安全的。
懒汉式单例:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance(){ if(instance == null){ // 1 instance = new Singleton(); // 2 } // 3 return instance; }}运用了延迟加载,在需要的时候进行初始化。 然而 1、2、3 整体不具有原子性,所以懒汉式单例应该不是线程安全的。
下面证明懒汉式单例是非线程安全的:
public class MyThread extends Thread{ public void run() { System.out.println(Singleton.getInstance().hashCode()); }}public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); }}运行得到:
2146509683 810456228 1996534722
结论:懒汉式单例运用延迟加载在需要时候进行初始化,保证了特定情况下其性能要优于饿汉式单例。然而它却是非线程安全的。
我们尝试修改代码,目的是把懒汉式单例修改成线程安全的。
第一次尝试 为 getInstance() 加锁:
public class Singleton { private static Singleton instance = null; synchronized public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; }}运行得到:
2000544445 2000544445 2000544445
这样修改可以保证线程安全性;但由于锁的独占性,多个线程频繁调用 getIntance() 很可能会阻塞,效率低下。
第二次尝试 继续缩小同步块的范围:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } return instance; }}运行得到:
略
hashcode() 返回值都一样。
这种方法基本等同于第一次尝试。
第三次尝试 继续缩小同步块的范围:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }}运行得到:
1175759956 2000544445 2146509683
相比前两次尝试,第三次尝试执行效率会有明显提升;但是破坏了原子性,不是线程安全的,得到的可能不是单例。
第四次尝试 双重检查锁定机制(double check lock)(DCL):
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}运行得到:
略
hashcode() 返回值都一样。
此方案综合了前面几次尝试的优点:
锁同步保证了原子性,保证线程安全性;第一次空检测对提升性能起到了很大的作用。但是真的实现了线程安全吗?并没有。
在 DCL 基础上,为变量 instance 加上 voltile 关键字,才算是真正的实现了线程安全:
public class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}运行得到:
略
hashcode()返回值都一样。
Q:回头来看,为什么必须加 volatile 关键字呢? A:重排序
结论:懒汉式单例,经过基于 volatile 的 DCL 进行改造,能够具有线程安全性。
volatile + synchronized 实现了线程安全性。DCL 的第一个空检测很大程度上优化了性能增加一个 final 域同样能解决问题:
public class Singleton { private static Singleton instance = null; private final int para; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } private Singleton(){ para = 1; }}写内存语义:在构造函数内对一个 final 域的写入,与随后将对象引用赋值给引用变量,这两个操作不能重排序; 实现原理:在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障; 写内存语义可以确保在对象的引用为任意线程可见之前,final 域已经被初始化过了。
读内存语义:初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作不能重排序; 实现原理:在读 final 域之前插入一个 LoadLoad 屏障。 读内存语义可以确保如果对象的引用不为 null,则说明 final 域已经被初始化过了。
总之,final 域的内存语义提供了初始化安全保证:只要 this 引用没有在构造函数中“逸出”,不需要同步就可以保证任意线程看到的都是初始化后的值。 注意:此时 Singleton 并非不可变,引入 final 域的目的是能够安全地初始化。
调用 getInstance() 导致 Holder 类被装载,Holder 对应的的 Class 类型的对象自动创建,JVM 会获得这个 Class 对象初始化锁,这个锁可以同步多个线程对同一个类的初始化。因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。
看图更形象一些:
可能得到的执行时序:
结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。
新闻热点
疑难解答