多线程条件下如何正确实现单例模式
单例模式是最为广泛使用的一种设计模式,其主要的目的就是保持一个类只有一个实例,其在单线程条件下实现比较简单,然而在多线程条件下,如何能够正确实现单例模式则需要对多线程的锁以及volatile关键字有所了解,接下来希望通过在多线程条件下实现单例模式来学习多线程的基础知识。
1.单线程版本单例模式实现
public class SingleThreadedSingleton {
// 保存该类的唯一实例
private static SingleThreadedSingleton instance = null;
// 省略实例变量声明
/*
* 私有构造器使其他类无法直接通过new创建该类的实例
*/
private SingleThreadedSingleton() {
// 什么也不做
}
/**
* 创建并返回该类的唯一实例
* 即只有该方法被调用时该类的唯一实例才会被创建
*
* @return
*/
public static SingleThreadedSingleton getInstance() {
if (null == instance) {// 操作①
instance = new SingleThreadedSingleton();// 操作②
}
return instance;
}
}
在单线程条件下,该单例模式的代码没有问题,但是放在放在多线程环境下则有可能出现问题。在getInstance()方法中使用了if条件判断,而if条件判断在多线程中形成一个check-then-act操作,这套操作并不是原子操作,因此在多线程条件下有可能出现线程间交错执行的情况。比如说线程1和线程2同时执行if(null == instance)的判断且两个线程都判断成功,此时线程1创建出一个instance的实例,紧接着线程2也会创建一个instance实例,这显然违背了单例模式的原则。为了解决这个问题,首先想到的办法就是进行加锁的操作。、
2.简单加锁的单例模式实现
public class SimpleMultithreadedSingleton {
// 保存该类的唯一实例
private static SimpleMultithreadedSingleton instance = null;
/*
* 私有构造器使其他类无法直接通过new创建该类的实例
*/
private SimpleMultithreadedSingleton() {
// 什么也不做
}
/**
* 创建并返回该类的唯一实例 <BR>
* 即只有该方法被调用时该类的唯一实例才会被创建
*
* @return
*/
public static SimpleMultithreadedSingleton getInstance() {
synchronized (SimpleMultithreadedSingleton.class) {
if (null == instance) {
instance = new SimpleMultithreadedSingleton();
}
}
return instance;
}
}
通过synchronized关键字加锁的方式实现单例模式显然是线程安全的,但是这意味着执行getInstance()方法的每一个线程都要申请锁,这样做的话很有很大的锁开销,为此应该想办法尽量减少锁的开销。一个有效的办法就是先检查instance是否为null,如果不为null则直接返回,不需要加锁,否则则加锁创建instance
3.基于双重检查锁定的错误单例模式实现
public class IncorrectDCLSingletion {
// 保存该类的唯一实例
private static IncorrectDCLSingletion instance = null;
/*
* 私有构造器使其他类无法直接通过new创建该类的实例
*/
private IncorrectDCLSingletion() {
// 什么也不做
}
/**
* 创建并返回该类的唯一实例 <BR>
* 即只有该方法被调用时该类的唯一实例才会被创建
*
* @return
*/
public static IncorrectDCLSingletion getInstance() {
if (null == instance) {// 操作①:第1次检查
synchronized (IncorrectDCLSingletion.class) {
if (null == instance) {// 操作②:第2次检查
instance = new IncorrectDCLSingletion();// 操作③
}
}
}
return instance;
}
}
通过两次判断,看似这种方式避免了多余的锁开销有保证的线程安全,如果线程1,线程2同时进行第一个if(null == instance)的判断,然后有一个线程将申请锁资源创建instance,之后另一个线程在获取锁资源再进行判断时instance将不会等于null,就不会再创建instance实例。但是线程在运行期间可能会遇到重排序的情形,比如instance == new IncorrectDCLSingletion()方法可以拆解为以下几步
- objRef = allocate(IncorrectDCLSingletion.class) // 操作1:分配对象所需的内存空间
- invokeConstructor(objRef) //操作2:初始化objRef引用的对象
- instance = objRef //操作3:将对象写入共享变量
根据锁的重排序规则,临界区内的操作可以在临界区内被重排序,因此上面的操作顺序可能重排为1 -> 3 -> 2, 由于在第一次判断时没有加锁,因此该线程可能看到一个未初始化的实例,因此线程在执行1时判断instance不为null直接返回instance而此时instance并未初始化完成,可能会出错。为了解决这个问题可以考虑使用volatile关键字来修饰instance。
4.基于双重校验锁定的正确单例模式实现
public class DCLSingleton {
/*
* 保存该类的唯一实例,使用volatile关键字修饰instance。
*/
private static volatile DCLSingleton instance;
/*
* 私有构造器使其他类无法直接通过new创建该类的实例
*/
private DCLSingleton() {
// 什么也不做
}
/**
* 创建并返回该类的唯一实例 <BR>
* 即只有该方法被调用时该类的唯一实例才会被创建
*
* @return
*/
public static DCLSingleton getInstance() {
if (null == instance) {// 操作①:第1次检查
synchronized (DCLSingleton.class) {
if (null == instance) {// 操作②:第2次检查
instance = new DCLSingleton();// 操作③
}
}
}
return instance;
}
}
由于volatile能够保证线程的可见性与有序性,voliate能够禁止voliate修饰的变量的写操作与该操作之前的任何读、写操作进行重排序,从而保证线程读取到的instance已经初始化完毕。当然除了这个方式外,还有其他一些方式可以实现多线程条件下的单例模式
5. 基于静态内部类的单例模式实现、
public class StaticHolderSingleton {
// 私有构造器
private StaticHolderSingleton() {
}
static class InstanceHolder {
// 保存外部类的唯一实例
static {
}
final static StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
}
public static StaticHolderSingleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
类的静态变量被初次访问时会触发Java虚拟机对该类进行初始化,及该类的静态变量的值会变为其初始值而不是默认值,因此静态方法getInstance()被调用时,Java虚拟机会初始化这个方法所访问的内部静态类InstanceHolder,使得InstanceHolder的静态变量被初始化,从而创建单一实例。
6. 基于枚举类型的单例模式实现
public class EnumBasedSingletonExample {
public static enum Singleton {
INSTANCE;
// 私有构造器
Singleton() {
}
}