多线程条件下如何正确实现单例模式
多线程条件下如何正确实现单例模式
单例模式是最为⼴泛使⽤的⼀种设计模式,其主要的⽬的就是保持⼀个类只有⼀个实例,其在单线程条件下实现⽐较简单,然⽽在多线程条件下,如何能够正确实现单例模式则需要对多线程的锁以及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()⽅法可以拆解为以下⼏步
1. objRef = allocate(IncorrectDCLSingletion.class) // 操作1:分配对象所需的内存空间
2. invokeConstructor(objRef) //操作2:初始化objRef引⽤的对象
3. 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() {
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论