单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
思考如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
通常单例模式在Java语言中,有两种构建方式:
1)懒汉方式:指全局的单例实例在第一次被使用时构建。
2)饿汉方式:指全局的单例实例在类装载时构建。
懒汉方式:
public class Singleton {
private static volatile Singleton INSTANCE = null;
// Private constructor suppresses
// default public constructor
private Singleton() {}
//thread safe and performance promote
public static Singleton getInstance() {
if(INSTANCE == null){
synchronized(Singleton.class){
//when more than two threads run into the first null check same time, to avoid instanced more than one time, it needs to be checked again.
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上面有几个重要的点,我们来看一下。
先介绍一下Java里的synchronized关键字,用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。是为了保证线程安全,因为如果有多个线程并行调用 getInstance() 的时候,可能会创建多个实例,并且很可能造成内存泄露问题。
好了,问题来了,我们为什么要使用synchronized关键字?当然是为了保证线程安全,因为如果有多个线程并行调用 getInstance() 的时候,可能会创建多个实例,并且很可能造成内存泄露问题。
好了,问题又来了,我们为什么要进行两次if(INSTANCE == null)的判断啊,用下面这样一次同步不可以了吗?
public static Singleton getInstance() {
synchronized(Singleton.class){
//when more than two threads run into the first null check same time, to avoid instanced more than one time, it needs to be checked again.
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
当然可以,但是创建对象的动作只有一次,后面的动作全是读取那个成员变量,读取的动作是不需要线程同步,这里进行线程同步,性能效率太低了!
怎么办呢?在线程同步前再加一个if(INSTANCE == null)的判断,如果对象已经创建了,那么就不需要线程的同步了。
于是一个叫双重检验锁模式(double checked locking pattern)的东东出现了,它是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
还有一个问题,为什么INSTANCE要声明为volatile?
那是因为在于INSTANCE = new Singleton()并非是一个原子操作, 事实上在 JVM 中这句话大概做了下面 3 件事情。
1)给 instance 分配内存
2)调用 Singleton 的构造函数来初始化成员变量
3)将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
所以要将 instance 变量声明成 volatile禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
当然你也可以将整个getInstance() 方法设为同步(synchronized),但是和之前那个原因一样,效率太低了。
饿汉模式:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
// Private constructor suppresses
private Singleton() {}
// default public constructor
public static Singleton getInstance() {
return INSTANCE;
}
}
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。但是,有时候我们需要在第一次getInstance()时才被创建。譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它。
Singleton 优雅版本:
public enum Singleton{
INSTANCE;
}
居然用枚举!!看上去好牛逼,通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。
默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。
参考:
https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F
http://www.runoob.com/design-pattern/singleton-pattern.html
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
http://coolshell.cn/articles/265.html
http://baike.baidu.com/item/synchronized