单例模式

来源:互联网 发布:淘宝网新款毛衣 编辑:程序博客网 时间:2024/06/01 07:44

饿汉式

这就是所谓的饥饿模式,因为单例的实例被声明成final和static变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

但是这可能带来潜在的性能问题:如果这个对象占用空间很大,没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。

第二种问题:Singleton实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那么饿汉式就无法使用。

public class Singleton { //类加载时就初始化    private final static Singleton instance = new Singleton();         private Singleton() {}         public static Singleton getInstance() {        return instance;    }}
针对这种情况,可以对饿汉式进行改进,使用一种新的设计思想——延迟加载 (Lazy-load Singleton)。 

懒汉式,线程不安全

懒汉式代码简单明了,而且使用了延迟加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作,即线程不安全。

public final class Singleton{    private static Singleton instance = null;         private Singleton() {}         public static Singleton getInstance() {        if(null == instance)            instance = new Singleton();        return instance;    }} 

懒汉式,线程安全

为了解决线程不安全的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public final class Singleton{    private static Singleton instance = null;     private Singleton() {}     public static synchronized Singleton getInstance() {        if(null == instance)            instance = new Singleton();        return instance;    }}

虽然做到了多实例线程安全,但是并不高效,每个线程调用 getInstance()都要加锁,并且其他线程需要检验锁是否存在。因为在任何时候只能有一个线程调用 getInstance()方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。创建实例时会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就有可能生成多个实例。

public final class Singleton{ // 线程不安全    private static instance = null;     private Singleton() {}     public static Singleton getInstance() {        if(null == instance) { //Single Checked            synchronized(Singleton.class) {                if(null == instance) //Double Checked                    instance = new Singleton();            }        }        return instance;    }}  
双重检验锁代码看起来很完美,很可惜,它是有问题。主要在于 instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面三件事情。
  • 给 instance 分配内存;
  • 调用 instance 的构造函数来初始化成员变量;
  • 将 instance 对象指向分配的内存空间(执行完这步instance就为非 null)。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,在 3 执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
解决指令重排序优化问题,只需要将 instance 变量声明成 volatile。volatile 修饰的话就可以确保 instance = new Singleton();对应的指令不会重排序,保证线程安全。
public class Singleton { // 线程安全    private volatile static Singleton instance; //声明成 volatile         private Singleton () {}         public static Singleton getSingleton() {        if (null == instance) {            synchronized(Singleton.class) {                if (null == instance) {                    instance = new Singleton();                }            }        }        return instance;    }}

静态内部类 static nested class

使用内部类来做到延迟加载对象,在初始化这个内部类的时候,由于 SingletonHolder 是私有的,除了 getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。这种写法最大的美在于,完全使用了 Java 虚拟机的机制进行同步保证,没有一个同步的关键字。
public class Singleton {     private static class SingletonHolder {        private final static Singleton INSTANCE = new Singleton();    }         private Singleton () {}     public final static Singleton getInstance() {        return SingletonHolder.INSTANCE;    }} 

枚举

实现单例模式的最佳方法。它不仅能避免多线程同步问题;而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化;而且不能通过 reflection attack 来调用私有构造方法。

public enum Singleton {    INSTANCE;    public void whateverMethod() {    }}
枚举类本质上是一种多例模式,只有有限个对象可以创建,而且无法显式调用构造函数创建对象。


0 0
原创粉丝点击