jmm的内存可见性

Java内存模型jmm

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

截屏2021-01-25 下午8.00.41

JMM中的主内存

  • 存储java实例对象
  • 包括成员变量、类信息、常量、静态变量等
  • 属于数据共享的区域,多线程并发操作时会引发线程安全问题

JMM中的工作内存

  • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
  • 变量会从主内存拷贝到工作内存,每个线程只能访问自己的工作内存
  • 其内还包括字节码行号指示器,Native方法信息。
  • 属于线程私有数据区域,不存在线程安全问题

JMM与Java内存区域

JMM与Java内存区域划分是不同的概念层次

  • JMM描述的是一组规则,围绕原子性,有序性,可见性展开,控制程序中各个变量在共享数据区域和私有数据区域的访问方式
  • 相似点:存在共享区域和私有区域
  • JMM中主内存是共享区域(在Java内存区域中应该包括方法区),工作内存是私有区域(在Java内存区域中应该包括程序计数器虚拟机栈本地方法栈

主内存与工作内存的数据存储类型及操作方式

  • 方法里的基本数据类型本地变量(局部变量)将直接存储在工作内存的栈帧结构中(如boolean,byte,char,int等)。
  • 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中(引用在栈帧,实例在堆中)。
  • 成员变量、static变量、类信息均会被存储在主内存中。
  • 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回住内存。

JMM如何解决可见性问题

截屏2021-01-25 下午8.47.55

把数据从内存加载到缓存、寄存器,运算结束后写回主内存

如此在多线程的时候会带来数据一致性问题

JMM为了数据一致性会进行指令重排序

需要满足的条件

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序
  • 无法通过happens-befor原则推导出来的,才能进行指令的重排序

happens-befor

A操作的结果需要对B操作可见,则A与B存在happeds-befor关系

例如:

1
2
i = 1; //线程A执行
j = i; //线程B执行

上面两行语句就存在happens-befor关系

八大原则

1、单线程happen-before原则:

  • 在同一个线程中,书写在前面的操作happen-before后面的操作。

2、锁的happen-before原则:

  • 同一个锁的unlock操作happen-before此锁的lock操作。

3、volatile的happen-before原则:

  • 对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。

4、happen-before的传递性原则:

  • 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

5、线程启动的happen-before原则:

  • 同一个线程的start方法happen-before此线程的其它方法。

6、线程中断的happen-before原则:

  • 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

7、线程终结的happen-before原则:

  • 线程中的所有操作都happen-before线程的终止检测。

8、对象创建的happen-before原则:

  • 一个对象的初始化完成先于他的finalize方法调用。

如果两个操作不满足上述任意一个规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。

如果操作A happen-befor操作B,那么操作A在内存上所作的操作对操作B都是可见的。

示例:
1
2
3
4
5
6
7
private int value = 0;
public void write(int input){
value = input;
}
public int red(){
return value;
}

如果线程A执行write操作,线程B执行red操作,且线程A优先于线程B执行

结果:这段代码均不满足happen-befor的条件,不是线程安全

修改方法:满足2:加入volatile修饰符或者满足3:对操作加锁

Volatile:JVM提供的轻量级同步机制

  • 保证被Volatile修饰的共享变量对所有线程总是可见的
  • 禁止指令重排序优化

Volatile的可见性

被Volatile修饰的变量对所有线程总是立即可见的,对Volatile变量的所有写操作总是能立即反映到其他线程中。但是对Volatile的运算在多线程环境中并不保证安全性

例如:

1
2
3
4
5
6
7
public class VolatileVisibility {
public static volatile int value = 0;

public static void increace(){
value++;
}
}

value变量的任何改变都会立即反映到线程中,但如果多个线程同时调用increace()方法就会出现线程安全问题,value++不具备原子性。

Volatile为什么立即可见

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中。

当读取volatile变量时,JMM会把该线程对应的工作内存置为无效。

Volatile如何禁止重排优化

内存屏障(Memory Barrier)

是一个CPU指令,其作用有两个

  1. 保证特定操作的执行顺序
    • 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
  2. 保证某些变量的内存可见性
    • 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

经典单例双重检测实现:

线程安全的单例写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {
// 不安全
// private static Singleton instance;
// 加上volatile禁止指令重排序,才安全
private volatile static Singleton instance;

private Singleton(){}

public static Singleton getInstance(){
//第一次检测
if(instance == null){
//同步
synchronized (Singleton.class){
if(instance == null){
//多线程环境下可能会出现问题的地方
instance = new Singleton();
}
}
}
return instance;
}
}

上述代码仍然又问题,在第一次检测时候可能读到instance不为空但是其还没彻底创建出来。

对象创建流程:
截屏2021-01-25 下午9.58.54

因为其不存在happen-befor,故这三条的顺序可能变成如下:

截屏2021-01-25 下午9.59.38

所以instance需要加volatile进行修饰,禁止其重排序

Volatile和Synchronized的区别:

截屏2021-01-25 下午10.02.02