JAVA多线程-懒汉式单例优化
JAVA指令重排
在讲解懒汉式之前,我们先讲一下Java中的指令重排。而Java设计了指令重排的这个机制其实目的就是为了加快指令的执行,优化执行速度。它的原理就类似于我们操作系统中的指令流水,如图:
下面我们讲一下在java中的指令重排实例:
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
/**
*情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1
*情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1
*情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4
*情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排
*/
Java-Volatile
在上面粗略谈完JAVA的指令重排后,我们接着讲一下JAVA中的Volatile。
Volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性):
- 保证可见性
- 不保证原子性
- 保证有序性(禁止指令重排)
在性能上,volatile是Java虚拟机提供的轻量级的同步机制,它跟public、private、protect这些修饰词对变量的读写操作相比读操作几乎无差写操作会相对慢一些,主要是因为它需要在执行的时候插入一些内存屏障指令来保证处理器不会发生因为指令重排导致乱序执行的现象发生。
内存屏障,包括读屏障和写屏障,我们继续沿用上文的代码例子进行讲解:
int num = 0;
volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {//读屏障 —》保证在该屏障之前的,对共享变量的改动,都同步到主存当中
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;//写屏障 -》保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据
}
JAVA多线程-懒汉式单例优化
首先,需要知道的一点是,有些类的实例化需要花费较多的资源与时间,并且它不一定会在启动的时候就需要使用到;于是就有了懒汉式单例模式,但是呢在多线程中按照正常的懒汉式单例模式来进行编码的话就会存在一些问题。
@Slf4j(topic = "thoseyears.Singleton")
public final class Singleton {
private Singleton() {
try {
log.debug("开始创建");
List<User> users = new ArrayList<>();
for(int i=0;i<9999999;i++){
users.add(new User("thoseyears"+i,i+""));
}
log.debug("创建完毕");
}catch (Exception e){
e.printStackTrace();
}
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
//1.1
//INSTANCE = new Singleton();
//1.2
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
首先在多线程中,上诉1.1的注释代码一个线程进入 if (instance == null) 判断语句,还没来得及往下执行,另外一个线程也进入了该判断语句,这时会产生两个实例,极大浪费了资源。
所以就有了1.2方案,对于这个过程进行加锁也就是常说的DCL(双端检锁)机制,但是即使是这样这个机制在多线程中也无法保证他的安全性,它会存在我们在上文提到的指令重排问题(synchronized 无法禁止指令重排)。比如:我们在对INSTANCE进行的实例化中,我们只是把地址等初始值赋值给INSTANCE,但是还没有调用它的构造方法来进行下一步初始化。这个时候又来了一个线程t2请求INSTANCE,但是这个时候INSTANCE已经有地址已经不是null了,但是由于 INSTANCE 实例未必已初始化,那么 t2 线程拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题。
指令重排导致的线程安全问题解决方案:
@Slf4j(topic = "thoseyears.Singleton")
public final class Singleton {
private Singleton() {
try {
log.debug("开始创建");
List<User> users = new ArrayList<>();
for(int i=0;i<9999999;i++){
users.add(new User("thoseyears"+i,i+""));
}
log.debug("创建完毕");
}catch (Exception e){
e.printStackTrace();
}
}
//加入Volatile字段
//只需要加入Volatile,利用它的读写屏障机制就可以解决这指令重排带来的线程安全问题
private static Volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE != null) {
return INSTANCE;
}
log.debug("getInstance");
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
return INSTANCE;
}
}