对象发布

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

我不喜欢这个世界,我喜欢你
原文地址:https://www.cnblogs.com/truekai/p/10861102.html