写点什么

【并发编程的艺术】详解单例模式的实现方式(Java)

发布于: 2021 年 02 月 01 日
【并发编程的艺术】详解单例模式的实现方式(Java)

系列文章:

【并发编程的艺术】JVM 体系与内存模型

【并发编程的艺术】JAVA 并发机制的底层原理

【并发编程的艺术】JAVA 原子操作实现原理

【并发编程的艺术】JVM 内存模型

【并发编程的艺术】详解指令重排序与数据依赖

【并发编程的艺术】Java 内存模型的顺序一致性


一 单例模式

单例模式是一种常用且常用来考察的设计模式。尤其在 Java 中,包含了对 JMM 理解的考察。事实上,尽管大家对单例模式概念都有了解,也能说出几种实现模式,但不一定能够保证正确,对原理也了解的不够透彻。所以本章将详细阐述。

二 概念

单例模式(Singleton Pattern)属于创建型模式,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。

  • 2、单例类必须自己创建自己的唯一实例。

  • 3、单例类必须给所有其他对象提供这一实例。

三 几种常见的实现方式

3.1 饿汉模式

public class Singleton {    private static Singleton instance = new Singleton();
private Singleton(){ } public static Singleton getInstance(){ return instance; }}
复制代码

优点:饿汉模式天生是线程安全的,使用时没有延迟。

缺点:启动时即创建实例,启动慢,有可能造成资源浪费。


3.2 饱汉模式(懒汉)

public class Singleton {  private static Singleton instance = null;  private Singleton() {  }  public static Singleton getInstance() {    if (instance == null) {	//1:A线程执行      instance = new Singleton();	//2:B线程执行    }    return instance;  }}
复制代码

优点:懒加载启动快,资源占用小,使用时才实例化,无锁。

缺点:非线程安全。

原因:当 A 线程执行代码 1 的同时,B 线程执行代码 2.此时,线程 A 可能会看到 Instance 引用的对象还没有完成的初始化(原因稍后描述)。

3.3 同步 getInstance 方法

基于 3.2,一种可以容易想到的方法是在方法上加 synchronized,做了同步处理之后,将不再会出现两个线程同时进入方法内的情况:

public class Singleton {        private static Singleton instance;        public synchronized static Singleton getInstance(){        if(instance == null){            instance = new Singleton();        }        return instance;    }}
复制代码

但显然,整个方法粒度上的锁过重,这会导致不必要的开销。事实上,我们只需要在 instance 被实例化前,以及实例化过程中加锁,在实例化之后就无需加锁了。所以,基于这一点考虑优化。

3.4 双重检查锁定

一种看似聪明的技巧,锁不在方法上,而是在内部通过判断 instance 是否是 null 来进行部分代码块的加锁,以此来降低锁的粒度:

public class DoubleCheckedLocking {
private static DoubleCheckedLocking instance;
public synchronized static DoubleCheckedLocking getInstance(){ if(instance == null){ //4:第一次检查 synchronized (DoubleCheckedLocking.class){ //5:加锁 if(instance == null){ //6:第二次检查 instance = new DoubleCheckedLocking(); //7:问题的根源出在这里 } } } return instance; }}
复制代码

但实际上,这是一个错误的优化!因为在线程执行到 4 时,代码读取到 instance 不等于 null,instance 引用的对象可能还没有完成初始化。

3.5 问题根源

我们在创建实例时,在 java 中只是一行代码: instance = new Singleton(); 但在汇编层面,代码会分为以下三步执行:

memory = allocate();	//1:分配内存空间ctorInstance(memory);	//2:初始化对象instance = memory; 	//3:设置instance指向刚分配的内存地址
复制代码

而在 2 和 3 之间,可能会出现指令重排序(在一些 JIT 编译器上,这种重排序是真实存在的)。发生重排序后,执行的顺序变为:

memory = allocate();	//1:分配内存空间instance = memory; 	//3:设置instance指向刚分配的内存地址,										//注意此时对象还没有被初始化!!!ctorInstance(memory);	//2:初始化对象
复制代码

根据 Java 语言规范,这里的重排没有影响单线程内的执行结果,所以是被允许的。但在上面 3.4 的单例模式中,可能会导致在多线程并发调用时,使用一个未完成初始化的实例,从而导致错误。

所以,知道问题根源后,可以想到两个解决方法来保证延迟初始化的线程安全:

1、不允许 2,3 重排序

2、允许 2,3 重排序,但不允许其他线程看到这个重排序

3.5.1 volatile

禁止指令重排,可以把 instance 声明为 volatile,代码如下:

public class DoubleCheckedLocking {
private volatile static DoubleCheckedLocking instance;
public synchronized static DoubleCheckedLocking getInstance(){ if(instance == null){ synchronized (DoubleCheckedLocking.class){ if(instance == null){ instance = new DoubleCheckedLocking(); } } } return instance; }}
复制代码

在把 instance 声明为 volatile 之后,将禁止 2,3 两步的指令重排序。但需要注意,需要 JDK 版本为 JDK5 以上!!!(开始使用新的 JSR-133 内存模型规范,加强了 volatile 的内存语义)。

3.5.2 基于类初始化

JVM 在类的初始化阶段(Class 加载后,被线程使用之前),JVM 会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化动作,避免一个类被初始化多次。基于这个特性,就有了另一种方法:

public class SafeDoubleCheckedLocking {
private static class InstanceHolder{ public static SafeDoubleCheckedLocking instance = new SafeDoubleCheckedLocking(); }
public static SafeDoubleCheckedLocking getInstance(){ return InstanceHolder.instance; }}
复制代码

当多线程并发执行时,这种方法的实现过程如下图所示:

也就是说,是通过允许初始化对象 和 设置引用指向内存两个步骤重排序,但不允许非构造线程”看到“这个重排序。

四 总结

本文列举了集中 Java 中实现单例模式的方法和代码,通过分析,可以知道各自实现的正确与否,以及错误原因。在有了前面系列文章的基础之后,对理解这个看似简单的单例代码实现会有更深刻的理解。

通过对比 volatile 的双重检查锁定方案,以及基于类初始化的方案,我们可以看到类初始化方案更简洁,但 volatile+双重检查锁定也有除了对静态字段实现延迟初始化外还可以对实例字段实现延迟初始化的优点。字段延迟初始化降低了初始化类或创建实例的开销,但同时也增加了访问被延迟初始化的字段的开销。大多时候,正常的初始化要优于延迟初始化。

所以,当需要对两种方案做一个最优选择时,那么就看如果:

1)确实需要对实例字段使用线程安全的延迟初始化,应该选择基于 volatile 的方案;

2)如果确实需要对静态字段使用线程安全的初始化,可使用基于类初始化的方案。

发布于: 2021 年 02 月 01 日阅读数: 23
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
【并发编程的艺术】详解单例模式的实现方式(Java)