关于多线程下的单例模式问题

镜花Q水月 发布于 07/07 20:40
阅读 112
收藏 0

初学java,最近在学习单例模式时,关于多线程的单例模式有些疑问,希望大佬帮忙解答。

最初的单例是这样的:

  1. public class Singleton {  
  2.   
  3.     /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
  4.     private static Singleton instance = null;  
  5.   
  6.     /* 私有构造方法,防止被实例化 */  
  7.     private Singleton() {  
  8.     }  
  9.   
  10.     /* 静态工程方法,创建实例 */  
  11.     public static Singleton getInstance() {  
  12.         if (instance == null) {  
  13.             instance = new Singleton();  
  14.         }  
  15.         return instance;  
  16.     }  
  17.   
  18.     /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
  19.     public Object readResolve() {  
  20.         return instance;  
  21.     }  
  22. }  
  23. 但是多线程下就会有问题,所以需要加锁,如果锁加在getInstance方法上,每次访问都会加锁,这样效率比较低,所以把锁加在if判断里面,代码如下:
  24. public static Singleton getInstance() {  
  25.         if (instance == null) {  
  26.             synchronized (instance) {  
  27.                 if (instance == null) {  
  28.                     instance = new Singleton();  
  29.                 }  
  30.             }  
  31.         }  
  32.         return instance;  
  33.     }  
  34. 但是文档说此时的代码看似没有问题,实际上还是会出错,原文如下:
  35. “在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

    a>A、B线程同时进入了第一个if判断

    b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

    c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

    d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

    e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。”

  36. 所以换成静态内部类来创建实例,因为JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。代码如下:

  37. public class Singleton {  
  38.   
  39.     /* 私有构造方法,防止被实例化 */  
  40.     private Singleton() {  
  41.     }  
  42.   
  43.     /* 此处使用一个内部类来维护单例 */  
  44.     private static class SingletonFactory {  
  45.         private static Singleton instance = new Singleton();  
  46.     }  
  47.   
  48.     /* 获取实例 */  
  49.     public static Singleton getInstance() {  
  50.         return SingletonFactory.instance;  
  51.     }  
  52.   
  53.     /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
  54.     public Object readResolve() {  
  55.         return getInstance();  
  56.     }  
  57. }  
  58. 这里我还可以理解,但是原文说还有一种实现方法也是可以的,只在创建类的时候进行同步,把创建和获取分开,代码如下:
  59. public class SingletonTest {  
  60.   
  61.     private static SingletonTest instance = null;  
  62.   
  63.     private SingletonTest() {  
  64.     }  
  65.   
  66.     private static synchronized void syncInit() {  
  67.         if (instance == null) {  
  68.             instance = new SingletonTest();  
  69.         }  
  70.     }  
  71.   
  72.     public static SingletonTest getInstance() {  
  73.         if (instance == null) {  
  74.             syncInit();  
  75.         }  
  76.         return instance;  
  77.     }  
  78. }  
  79. 以及后面同步更新属性的“影子实例”,都是使用这种方式,这个方法和最初的同步方法相比,是把创建对象单独抽取了一个方法,然后加锁,这里并没有静态类,为什么不会出现如上所述的A、B线程同时进入 if (instance == null)的判断,然后A线程没有初始化完成,B线程就把实例返回的情况呢?不明白单独抽取方法有什么效果。还有个小问题,第一种方式的readResolve方法,注释说可以保证对象序列化前后保持一致,虽然学习过序列化流,但是不明白这个方法是如何使用的,希望大佬指点,万分感谢!!!
加载中
返回顶部
顶部