0.摘要
主要讨论了在多线程并发环境下,安全发布的几种方式(多种单例模式演示 )。
1.基本概念
发布对象:使对象能够被当前范围之外的代码所看见。比如通过类的非私有方法返回对象的引用。
对象逸出:一种错误的发布。当一个对象还没有构造完成时,就被其他线程所看见
不正确的发布对象会导致两种错误:
1)发布线程以外的任何线程都可以看到被发布对象的过期的值
2)线程看到的被发布线程的引用是最新的,然而被发布对象的状态却是过期的
如何安全发布对象?四种方法
1)在静态初始化函数中初始化一个对象引用
2)将对象的引用保存到volatile类型域或者AtomicReference对象中
3)将对象的引用保存到一个由锁保护的域中
4)将对象的引用保存到某个正确构造对象的final类型域中(本文目前没涉及到)
2.懒汉模式
(1)最简单的一段:在第一次使用的时候创建
1 public class SingletonExample1 { 2 private SingletonExample1() { 3 } 4 private static SingletonExample1 instance = null; 5 6 public static SingletonExample1 getInstance() { 7 if (instance == null) { 8 instance = new SingletonExample1(); 9 } 10 return instance; 11 } 12 }
分析:单线程的情况下,线程是安全的。在多线程的情况下,第7-9行代码会出现线程不安全的问题。两个线程同时调用这段代码的时候会拿到两个不同的实例,尽管并不会产生什么不好的情况,但是如果这个私有构造方法在实现的时候要做很多操作,如资源的释放、运算等。那么这样产生的单例是线程不安全的。
(2)推荐和线程安全一段:上述可以单独使用synchronized修饰getInstance()来保证线程安全,但是有可能会造成性能损耗。更好的方法
1 /** 2 * 懒汉模式-双重同步锁单例模式 3 * 单例在使用时候创建 4 */ 5 public class SingletonExample1 { 6 private SingletonExample1() { 7 } 8 9 //单例对象 10 private static SingletonExample1 instance = null; 11 12 //静态工厂方法 13 public static SingletonExample1 getInstance() { 14 if (instance == null) {//双重检测机制 15 synchronized (SingletonExample1.class) {//同步锁 16 if (instance == null) { 17 instance = new SingletonExample1(); 18 } 19 } 20 } 21 return instance; 22 } 23 }
分析:但上述的代码并不是线程安全的,插播一个小知识点。
这里有一个知识点:CPU指令相关 在上述代码中,执行new操作的时候,CPU一共进行了三次指令 (1)memory = allocate() 分配对象的内存空间 (2)ctorInstance() 初始化对象 (3)instance = memory 设置instance指向刚分配的内存
在程序运行过程中,CPU为提高运算速度和JVM优化会做出违背代码原有顺序的优化。我们称之为乱序执行优化或者说是指令重排。
那么上面知识点中的三步指令极有可能被优化为(1)(3)(2)的顺序。当我们有两个线程A与B,A线程遵从132的顺序,经过了两此instance的空值判断后,执行了new操作,并且cpu在某一瞬间刚结束指令(3),并且还没有执行指令(2)。而在此时线程B恰巧在进行第一次的instance空值判断,由于线程A执行完(3)指令,为instance分配了内存,线程B判断instance不为空,直接执行return,返回了instance,这样调用这个实例的时候会出现错误。
如何解决指令重排?
在对象声明时使用volatile(双重检测)关键字修饰,阻止CPU的指令重排。 private volatile static SingletonExample instance = null;
3.饿汉模式
(1)最简单的一段:在类装载的时候创建,能保证线程安全
1 //饿汉模式,在类装载的时候创建 2 public class SingletonExample1 { 3 private SingletonExample1() { 4 } 5 6 private static SingletonExample1 instance = new SingletonExample1(); 7 8 public static SingletonExample1 getInstance() { 9 return instance; 10 } 11 }
分析:第一如果其私有构造函数中存在大量处理,那么类装载的时间会很长,第二如果只进行了类的加载却没有调用,会造成性能的浪费。(下面的分析有点长)
(2)通过静态块来初始化,注意静态代码块一定要在静态工厂方法的上面
1 public class SingletonExample { 2 // 私有构造函数 3 private SingletonExample() { 4 } 5 // 单例对象 6 private static SingletonExample instance = null; 7 static { 8 instance = new SingletonExample(); 9 } 10 // 静态的工厂方法 11 public static SingletonExample getInstance() { 12 return instance; 13 } 14 }
4.枚举模式
1 /** 2 *枚举模式 3 */ 4 public class SingletonExample2 { 5 private SingletonExample2() { 6 } 7 8 public static SingletonExample2 getInstance() { 9 return Singleton.INSTANCE.getSingleton(); 10 } 11 12 private enum Singleton { 13 INSTANCE; 14 private SingletonExample2 singleton; 15 16 //JVM来保证这个方法只被调用一次 17 Singleton() { 18 singleton = new SingletonExample2(); 19 } 20 21 public SingletonExample2 getSingleton() { 22 return singleton; 23 } 24 } 25 26 }
分析:由于枚举类的特殊性,JVM可以保证枚举类的构造函数只被调用一次,相比饿汉模式,在实际调用的时候才做初始化。相比懒汉模式更加安全。推荐使用。
5.小结
本文章是作者学习并发编程中的笔记总结,方面我温故,希望能帮到大家。主要总结了几种创建单例的方法,推荐使用枚举模式。
2019-05-14 16:09:33