你真的会写单例模式吗——Java实现

饿汉法html


顾名思义,饿汉法就是在第一次引用该类的时候就建立对象实例,而无论实际是否须要建立。代码以下:java

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

这样作的好处是编写简单,可是没法作到延迟建立对象。可是咱们不少时候都但愿对象能够尽量地延迟加载,从而减少负载,因此就须要下面的懒汉法:android

单线程写法

这种写法是最简单的,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,若是是null就new一个出 来,最后返回singleton对象。这种方法能够实现延时加载,可是有一个致命弱点:线程不安全。若是有两条线程同时调用getSingleton() 方法,就有很大可能致使重复建立对象。web

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) singleton = new Singleton();
        return singleton;
    }
}

考虑线程安全的写法

这种写法考虑了线程安全,将对singleton的null判断以及new的部分使用synchronized进行加锁。同时,对 singleton对象使用volatile关键字进行限制,保证其对全部线程的可见性,而且禁止对其进行指令重排序优化。如此便可从语义上保证这种单例 模式写法是线程安全的。注意,这里说的是语义上,实际使用中仍是存在小坑的,会在后文写到。缓存

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

兼顾线程安全和效率的写法

虽然上面这种写法是能够正确运行的,可是其效率低下,仍是没法实际应用。由于每次调用getSingleton()方法,都必须在synchronized这里进行排队,而真正遇到须要new的状况是很是少的。因此,就诞生了第三种写法:安全

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;
    }    
}

这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()方法中,进行两次null检查。看似画蛇添足,但实际上却极大提高了 并发度,进而提高了性能。为何能够提升并发度呢?就像上文说的,在单例中new的状况很是少,绝大多数都是能够并行的读操做。所以在加锁前多进行一次 null检查就能够减小绝大多数的加锁操做,执行效率提升的目的也就达到了。多线程

那么,这种写法是否是绝对安全呢?前面说了,从语义角度来看,并无什么问题。可是其实仍是有坑。说这个坑以前咱们要先来看看volatile这个 关键字。其实这个关键字有两层语义。第一层语义相信你们都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会立刻由工做内存(Work Memory)写回主内存(Main Memory),因此会立刻反应在其它线程的读取操做中。顺便一提,工做内存和主内存能够近似理解为实际电脑中的高速缓存和主存,工做内存是线程独享的, 主存是线程共享的。volatile的第二层语义是禁止指令重排序优化。你们知道咱们写的代码(尤为是多线程代码),因为编译器优化,在实际执行的时候可 能与咱们编写的顺序不一样。编译器只保证程序执行结果与源代码相同,却不保证明际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线 程,这种乱序就可能致使严重问题。volatile关键字就能够从语义上解决这个问题。并发

注意,前面反复提到“从语义上讲是没有问题的”,可是很不幸,禁止指令重排优化这条语义直到jdk1.5之后才能正确工做。此前的JDK中即便将变 量声明为volatile也没法彻底避免重排序所致使的问题。因此,在jdk1.5版本前,双重检查锁形式的单例模式是没法保证线程安全的。高并发

静态内部类法

那么,有没有一种延时加载,而且能保证线程安全的简单写法呢?咱们能够把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就建立对象,而且因为静态内部类只会被加载一次,因此这种写法也是线程安全的:性能

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
    
    private Singleton(){}
        
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

可是,上面提到的全部实现方式都有两个共同的缺点:

  • 都须要额外的工做(Serializable、transient、readResolve())来实现序列化,不然每次反序列化一个序列化的对象实例时都会建立一个新的实例。

  • 可能会有人使用反射强行调用咱们的私有构造器(若是要避免这种状况,能够修改构造器,让它在建立第二个实例的时候抛异常)。

枚举写法

固然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法:

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚举除了线程安全和防止反射强行调用构造器以外,还提供了自动序列化机制,防止反序列化的时候建立新的对象。所以,Effective Java推荐尽量地使用枚举来实现单例。

总结

这篇文章发出去之后获得许多反馈,这让我受宠若惊,以为应该再写一点小结。代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不一样的平台、不一样的开发环境(尤为是jdk版本)下,天然有不一样的最优解(或者说较优解)。
好比枚举,虽然Effective Java中推荐使用,可是在Android平台上倒是不被推荐的。在这篇Android Training中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再好比双重检查锁法,不能在jdk1.5以前使用,而在Android平台上使用就比较放心了(通常Android都是jdk1.6以上了,不只修正了volatile的语义问题,还加入了很多锁优化,使得多线程同步的开销下降很多)。

最后,无论采起何种方案,请时刻牢记单例的三大要点:

  • 线程安全

  • 延迟加载

  • 序列化与反序列化安全

参考资料

《Effective Java(第二版)》
 《深刻理解Java虚拟机——JVM高级特性与最佳实践(第二版)》

原文参考:http://www.tekbroaden.com/singleton-java.html

相关文章
相关标签/搜索