文章目录
单例(Singleton)概述
使单例模式是为了:保证一个类只有一个实例,并且提供这个实例的全局访问点
一般做法是把该单例实例作为该类的一个静态变量、构造方法私有化,至于如何取得和创建该对象,有两种种方法实现:懒汉式和饿汉式
对于不太了解静态变量和静态方法的可以点这里
Ⅰ 懒汉式-线程不安全
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null)
,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton();
语句,这将导致实例化多次 uniqueInstance。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅱ 饿汉式-线程安全
线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
Ⅲ 懒汉式与饿汉式横评
懒汉式😂很懒,拖延症,不到必要时候不创建单例实例 | 饿汉式😂饿了,一上来(类加载时)就创建单例 | |
---|---|---|
线程安全(多线程环境下能否确保对象仅被创建一次) | 不安全 | 安全 |
单例实例创建时机 | 当Singleton.getUniqueInstance()方法被调用时才会创建实例,如果程序由始至终都没有调用该方法,则单例实例永远不会被创建,节省了内存空间和资源 | 在Singleton被类加载器加载时创建单例对象,并且会把单例变量一直存放在内存的方法区中,直到程序结束才会把单例对象销毁,如果程序由始至终都没有使用该实例,将会造成资源浪费 |
由于懒汉式有更好的性能,所以接下介绍两种通过加锁使得懒汉式在多线程环境中也能线程安全的方法:
Ⅳ 懒汉式-线程安全改进(通过加锁的方法)
单锁 getUniqueInstance() 方法
只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
双重校验锁-线程安全
uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。
双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 uniqueInstance == null 时两个线程同时进行实例化操作。
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
相信看到这里的同学的专研精神已经到了很高的地步了。但如果有同学想深入了解volatile和synchronized的可以点进去看看。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
后记(对于volatile作用的疑惑)
笔者最近发现有些读者有这样的疑惑:在双重所检验中uniqueInstance
使用volatile
关键字是因为volatile
关键字能够让对象被修改时 第一时间通知其他线程。
但其实并非这样。如果T1 线程在执行的 new 的 1、3 步之后迟迟不跑第 2 步,那么线程 T2 仍然会用还没有初始化uniqueInstance
执行业务逻辑,最后就会报错。
所以使用 volatile 的作用是禁止 JVM 的指令重排,没毛病。👍