设计模式——单例模式

关于基本的懒汉式,饿汉式等写法网上介绍多如牛毛,这里不再赘述直接讨论加了volatile关键字的双重锁(Double check),静态内部类以及枚举等写法,如有不对,恳请读者指出,欢迎讨论。

1.加了volatile关键字的双重锁:
public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }   
}

 

  相信对单例模式有过了解的同学都知道,有了双重锁,效率应该是没问题的了,但是线程安全就不一定能保证的了。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。
  编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。原因在于singleton = new Sngleton();这句并非一个原子操作,实际上JVM在这句话中大概做了是三件事情:
1.给singleton分配内存;
2.调用Singleton的构造函数初始化成员变量,形成实例;
3.将singleton对象指向分配的内存空间(执行完这步singleton才是非null的)
  由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。最终执行顺序可能是1-2-3,也可能是1-3-2,如果是后者,在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是未经初始化的对象,程序就会出错。(所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。)而volatile关键字就可以从语义上解决这个问题。使用volatile有两个功能:
1.这个变量不会在多个线程中存在副本,直接从内存中读取。
2.这个关键字会禁止指令重排优化也就是说,在volatile变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。

下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

 
2.静态内部类(嵌套类):
public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
} 

 

 
  把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,实现了延迟加载,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。
 
3.枚举
  使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。
public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

 

最后,不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
参考:
    《Thinking in Java》 
    《深入理解Java虚拟机》
    《Effective Java》
    http://coolshell.cn/articles/265.html
    http://www.iteye.com/topic/652440
    http://www.cnblogs.com/dolphin0520/p/3920373.html
posted @ 2017-02-21 23:01  night_joe  阅读(209)  评论(0编辑  收藏  举报