两大特性:线程之间可见性 与 禁止指令重排序
线程之间可见性:
线程操作共享的主内存中资源都是先将其从主内存读取到自己的工作内存中(正常情况下各线程的工作内存之间是相互隔离的、不可见的),操作完成后再写回至主内存中,在中途一个线程修改了该资源写回到主内存后,其他线程不会知道,还是会依然使用第一次读到工作内存中的资源进行操作。添加volatile关键字,会实现线程之间操作的可见性,也就是每个线程发生每次操作后写回到主内存中后,cpu总线嗅探机制嗅探到工作内存与主内存中数据出现不一致情况,会使工作内存中该不一致数据实现,在下次使用该资源时会第一时间去主内存中再读取一次新值进行使用,实现了线程之间的可见性。
JMM:java内存模型,每个线程有自己的工作内存,工作内存中存放一份主内存数据副本,对副本操作后再写入主内存。但会导致缓存一致性问题
MESI:缓存一致性协议。
禁止指令重排序:
cpu在执行指令的时候为了提高效率,会将先后执行不影响最终结果的指令进行重排序,存在后边指令先执行的情况,但重排序在多线程的情况下有可能会出现问题,比如多个线程相互依赖某些值,但发生了指令重排序导致依赖的值不准确,可以使用volatile关键字禁止指令重排序。
以一个DCL(double check lock)的懒汉式单例设计模式为例:
public class User{
// volatile禁止指令重排
private volatile static User user;
// 双重检验锁 DCL 懒汉式单例
public static User getInstance() {
if (user == null) {
// 加锁
synchronized (User.class) {
if (user == null) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 创建对象
user = new User();
// 创建对象分为三个步骤 1.分配堆内存(半初始化状态)
2.初始化数据
3.栈中变量指向堆中地址
// 若无volatile修饰,发生指令重排序,上面2.3步骤颠倒执行,
// 此时刚好另一个线程判断user != null,则该线程获取了半初始化状态的user对象
}
}
}
return user;
}
volatile实现:
代码中添加volatile关键字,在编译时会添加ACC_VOLATILE标识,jvm执行时看到该标识后会添加jvm内存读写屏障来禁止指令重排(屏障两边的指令不能重排序)。