辨析六种单例模式

参考: http://www.zuoxiaolong.com/blog/article.ftl?id=124

第一种: 不考虑并发访问

 1 public class Singleton {
 2     // 初始化静态的实例
 3     private static Singleton singleton;
 4     // 私有化构造函数
 5     private Singleton(){}
 6     // 提供一个静态方法返回静态实例
 7     public static Singleton getInstance(){
 8         if (singleton == null) {
 9             singleton = new Singleton();
10         }
11         return singleton;
12     }
13 }

这种单例模式不考虑并发访问的情况, 存在线程安全问题.

原因说明: 当两个线程A执行getInstance方法时, singleton判断为null, 进入if块内部, 此时CPU执行权切换到线程B, 线程B也执行getInstance方法, singleton判断也为null, 也进入if块内部, 并实例化了singleton, CPU执行权在切回线程A, 线程A也实例化了singleton, 这时问题就出来了, 有两个线程都进入了if块去创造实例, 结果就造成单例模式并非单例.

第二种: 同步获取实例的方法

 1 public class BadSynchronizedSingleton {
 2 
 3     private static BadSynchronizedSingleton synchronizedSingleton;
 4 
 5     private BadSynchronizedSingleton(){}
 6 
 7     public synchronized static BadSynchronizedSingleton getInstance(){
 8         if (synchronizedSingleton == null) {
 9             synchronizedSingleton = new BadSynchronizedSingleton();
10         }
11         return synchronizedSingleton;
12     }
13 }

将整个获取实例的方法同步, 这样在一个线程访问这个方法时, 其它所有的线程都要处于挂起等待状态, 避免了刚才同步访问创造出多个实例的危险.

缺点: 这样的设计会造成很多无谓的等待.

第三种: 双重加锁

 1 public class Singleton {
 2 
 3     private static Singleton singleton ;
 4 
 5     private Singleton(){}
 6 
 7     public static Singleton getInstance(){
 8         if (singleton== null) {
 9             synchronized (Singleton.class) {
10                 if (singleton== null) {
11                     singleton= new singleton();
12                 }
13             }
14         }
15         return singleton;
16     }
17 }

为什么synchronized 块中需要使用if判断singleton是否为null?

当两个线程A和B同时进入第一个if判断后, 线程A获得CPU执行权, 拿到线程锁之后, 进入同步块实例化singleton, 线程A退出了同步块, 返回了第一个创造的实例, 这时CPU执行权切到线程B, 线程B拿到线程锁之后也进入了同步块实例化singleton, 此时如果没有判断singleton是否为空, 会导致线程B也实例化singleton, 导致单例模式并非单例.

存在问题:

JVM创建对象并非是原子性操作

在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误.

JVM创建新的对象时,主要要经过三步:

  1) 分配堆内存

  2) 初始化构造器

  3) 将对象引用(栈中)指向分配的堆内存的地址

这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。

但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题:

  1) 分配内存

  2) 将对象引用(栈中)指向分配的内存的地址

  3) 初始化构造器

因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给singleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为singleton对象已经实例化了,直接返回一个引用。如果初始化构造器之前,这个线程使用了singleton,就会产生莫名的错误

第四种: 标准模式(推荐)

 1 public class Singleton {
 2     
 3     private Singleton(){}
 4     
 5     public static Singleton getInstance(){
 6         return SingletonInstance.instance;
 7     }
 8     
 9     private static class SingletonInstance{
10         
11         static Singleton instance = new Singleton();
12         
13     }
14 }

一个类的静态属性在第一次加载类时初始化,这是JVM帮我们保证的,所以无需担心并发访问的问题.

上述形式保证: 

1) 不考虑反射情况, Singleton最多只有一个实例

2) 并发访问情况下, 不会产生多个实例

3) 并发访问情况下, 不会因初始化动作未完全完成而造成使用了尚未正确初始化的实例

第五种: 饿汉式加载

public class Singleton {

    private Singleton() {}
    
    // 初始化并实例化singleton 
    private static Singleton singleton = new Singleton();

    public static Singleton getInstance(){
        return Singleton.singleton;
    }
}

上述方式与我们最后一种给出的方式类似,只不过没有经过内部类处理,这种方式最主要的缺点就是一旦我访问了Singleton的任何其他的静态域,就会造成实例的初始化,而事实是可能我们从始至终就没有使用这个实例,造成内存的浪费

不过在有些时候,直接初始化单例的实例也无伤大雅,对项目几乎没什么影响,比如我们在应用启动时就需要加载的配置文件等,就可以采取这种方式去保证单例.

第六种: 静态的实例属性加上关键字volatile

关键字volatile的作用是标识这个属性是不需要优化的.

这样也不会出现实例化发生一半的情况,因为加入了volatile关键字,就等于禁止了JVM自动的指令重排序优化,并且强行保证线程中对变量所做的任何写入操作对其他线程都是即时可见的.

volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作.

volatile关键字是在JDK1.5以及1.5之后才被给予了意义,所以这种方式要在JDK1.5以及1.5之后才可以使用.

不推荐这种方式,一是因为代码相对复杂,二是因为由于JDK版本的限制有时候会有诸多不便.

 

posted on 2018-08-25 13:49  ert999  阅读(114)  评论(0编辑  收藏  举报

导航