定义
JMM即Java内存模型(Java memory model),在JSR133里指出了JMM是用来定义一个统一的、跨平台的内存模型,是缓存一致性协定,用来定义数据读写的规定。
内存可见性
在Java中,不同线程领有各自的公有工作内存,当线程须要读取或批改某个变量时,不能间接去操作主内存中的变量,而是须要将这个变量读取到线程的工作内存的变量正本中,当该线程批改其变量正本的值后,其它线程并不能立即读取到新值,须要将批改后的值刷新到主内存中,其它线程能力从主内存读取到批改后的值。
指令重排序!
在执行程序时为了进步性能,编译器和处理器经常会对指令做重排序,指令重排序使得代码在多线程执行时会呈现一些问题。
其中最驰名的案例便是在初始化单例时因为可见性和重排序导致的谬误。
单例模式
案例1
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
以上代码是经典的懒汉式单例实现,但在多线程的状况下,多个线程有可能会同时进入if (singleton == null)
,从而执行了屡次singleton = new Singleton()
,从而毁坏单例。
案例2
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上代码在检测到singleton
为null后,会在同步块中再次判断,能够保障同一时间只有一个线程能够初始化单例。但依然存在问题,起因就是Java中singleton = new Singleton()
语句并不是一个原子指令,而是由三步组成:
- 为对象分配内存
- 初始化对象
- 将对象的内存地址赋给援用
然而当通过指令重排序后,会变成:
- 为对象分配内存
- 将对象的内存地址赋给援用(会使得singleton != null)
- 初始化对象
所以就存在一种状况,当线程A曾经将内存地址赋给援用时,但实例对象并没有齐全初始化,同时线程B判断singleton
曾经不为null,就会导致B线程拜访到未初始化的变量从而产生谬误。
案例3
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上代码对singleton
变量增加了volatile
润饰,能够阻止部分指令重排序。
那么为什么volatile能够保障变量的可见性和阻止指令重排序?
volatile
原理
- 规定线程每次批改变量正本后立即同步到主内存中,用于保障其它线程能够看到本人对变量的批改
- 规定线程每次应用变量前,先从主内存中刷新最新的值到工作内存,用于保障能看见其它线程对变量批改的最新值
- 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来避免指令重排序。
留神:
- volatile只能保障根本类型变量的内存可见性,对于援用类型,无奈保障援用所指向的理论对象外部数据的内存可见性。对于援用变量类型详见:Java的数据类型。
- volilate只能保障共享对象的可见性,不能保障原子性:假如两个线程同时在做x++,在线程A批改共享变量从0到1的同时,线程B曾经正在应用值为0的变量,所以这时候可见性曾经无奈发挥作用,线程B将其批改为1,所以最初后果是1而不是2。
感谢您浏览本文,关注我的公众号“语冰Yubing”可接管最新推送,外面也有我分享的一些优质资源。