JMM
- 原子性:保证指令的执行不受线程上下文切换的影响
- 可见性:保证指令的执行不会收到CPU cache的影响
- 有序性:保证指令执行顺序不会受到cpu指令并行优化的影响
保证可见性
例子
package com.huangbei.test1;
import com.huangbei.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test5")
public class Test5 {
volatile static boolean flag=true;
public static void main(String[] args) {
new Thread(()->{
while (flag){
;
}
},"t1").start();
Sleeper.sleep(1);
log.debug("修改flag=false");
flag=false;
}
}
在上面的测试中,主线程对flag进行了修改,但是t1线程读到的flag一直都是true,因为t1每次读的flag值都是自己的工作内存中对初始化的flag的一份拷贝,导致t1线程无法结束运行。
解决(volatile或者synchronized)
- 对共享变量(成员变量)使用
volatile
关键字,保证线程每次读到的变量值都是最新值。 - 对共享变量加锁
synchronized
(线程获取锁后,线程工作内存中对共享变量的拷贝全部失效,线程必须从主内存读取共享变量的值;释放锁时,会将工作内存中的值同步到主内存中。)
注意:volatile
只能保证可见性,不能保证原子性,适合于一个线程写、多个线程读的情景。synchronized
可以保证可见性和原子性,但属于重量级的操作,性能较低。
避免指令重排序
指令重排序是保证结果正确的情况下对代码执行的先后顺序进行调整,以达到较大的指令执行吞吐量,在单线程程序不会引起问题,但在多线程程序中会引起意外结果,使用volatile
关键字修饰变量可以保证使用变量之前的代码不会重排序而是按照java代码的先后顺序执行。
volatile原理
volatile
底层实现原理是——内存屏障,Memory Barrier。
volatile
修饰的变量写指令后会设置写屏障volatile
修饰的变量读指令后会设置读屏障
`volatile`只能保证可见性和有序性,无法保证原子性。
- 如何保证可见性?
- 写屏障之前,对共享变量的改动会从工作内存同步到主存中
- 读屏障之后,对所有共享变量的读取都会读取主存中的最新数据而不是工作内存中的数据
- 如何保证有序性?
- 写屏障之前的代码不会在写屏障之后执行。
- 读屏障之后的代码不会在读屏障之前执行。
单例模式中volatile的应用
应用单例模式的类,始终只能创建一个实例:
public class Singleton {
private static Singleton instance=null;
private Singleton() { }
//静态方法
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
从上面的例子中,得到这些信息:构造方法是私有的,可以防止通过new
关键字创建实例,要想获取这个类的实例,只能通过getInstance()
方法获取,并且考虑了多个线程获取实例,在里面加上了synchronized
关键字保证互斥。
上面的类是线程安全的,但是效率不高,因为getInstance()
方法每次都会执行到同步代码块里面,意味着每次被调用都要加锁、解锁,而double-checked locking改进了这个问题:
public class Singleton {
private static Singleton instance=null;
private Singleton() { }
//静态方法
public static Singleton getInstance() {
if(instance==null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
return instance;
}
}
在synchronized
层加了一个判断逻辑,当两个线程同时调用这个方法,假设这个实例依然是null,那么t1、t2都会执行到外层if里面,但只有一个线程能拿到锁,另一个线程只能阻塞。当竞争到锁的线程释放锁,意味着instance
已经被实例化了,阻塞的线程执行到同步块里面时,由于instance不为null,最后返回的就是instance的的非Null引用。以后再获取这个类的实例,由于instance不为null,不会执行到外层if里面,就不用进入同步代码块,提高了效率。
有问题?
实际上,由于指令重排序,上面的double-checked locking会有问题。
- 造成的问题是:某个线程在获取到对象的实例引用后,这个引用指向的可能是一个未初始化的实例。
- 造成这个问题的原因是:当线程t1在执行
new Singleton()
这个构造方法时,可能先给instance
变量赋值,再进行初始化(指令重排序)。如果t2线程在t1线程初始化之前执行,那么在t2执行到外层if时,if不为空,t2直接返回这个instance,但这个instance指向的对象还没有初始化。
如何解决?
从问题出发,既然指令重排序会引起问题,那么用volatile
修饰变量禁止指令重排序就可以解决这个问题。
public class Singleton {
private volatile static Singleton instance=null;
private Singleton() { }
//静态方法
public static Singleton getInstance() {
if(instance==null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
happens-before规则
一种懒汉式的线程安全的单例模式类
package com.huangbei.test1;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class Singleton {
private Singleton() { }
private static class LazyHolder{
static final Singleton instance =new Singleton();
}
public static Singleton getInstance(){
return LazyHolder.instance;
}
}