Java内存模型和Volatile

一、Java内存模型(JMM)

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和在内存中取出变量的底层细节,是围绕着在并发过程中如何处理原子性,可见性和有序性这3个特性建立的

JMM规则

变量包含实例字段,静态字段,构成数组对象的元素,不包含局部变量和方法参数。

变量都存储在主内存

每个线程都有自己的工作内存,工作内存保存了被该线程使用到的变量的主内存副本拷贝

线程对变量的所有操作都只能在工作内存,不能直接读写主内存的变量

不同线程之间无法之间访问对方工作内存中的变量

主内存,线程,工作内存关系图

定义一个静态变量:static int a = 1;
线程A工作内存 | 指向 | 主内存 | 操作
-- | -- | -- | --
-- | -- | a = 1 | --
a = 1 | <-- | a = 1 | 线程A拷贝主内存变量副本
a = 3 | -- | a = 1 | 线程A修改工作内存变量值
a = 3 | --> | a = 3 | 线程A工作内存变量存储到主内存变量

上面的一系列内存操作,在JMM中定义了8种操作来完成

JMM交互

主内存和工作内存之间的交互,JMM定义了8种操作来完成,每个操作都是原子性的

lock(锁定):作用于主内存变量,把一个变量标识为一条内存独占的状态

unlock(解锁):作用于主内存变量,把lock状态的变量释放出来,释放出来后才能被其他线程锁定

read(读取):作用于主内存变量,把一个变量的值从主内存传输到工作内存中

load(载入):作用于工作内存变量,把read操作的变量放入到工作内存副本中

use(使用):作用于工作内存变量,把工作内存中的变量的值传递给执行引擎,每当虚拟机遇到需要这个变量的值的字节码指令时都执行这个操作

assgin(赋值):作用于工作内存变量,把从执行引擎收到的值赋值给工作内存变量,每当虚拟机遇到需要赋值变量的值的字节码指令时都执行这个操作

store(存储):作用于工作内存变量,把工作内存中的一个变量值,传送到主内存

write(写入):作用于主内存变量,把store操作的从工作内存取到的变量写入主内存变量中

二、volatile

当引入线程B的时候
定义一个静态变量:static int a = 1;
操作顺序 | 线程A工作内存 | 线程B工作内存 | 指向 | 主内存 | 操作
-- | -- | -- | -- | -- | --
-- | -- | -- | -- | a = 1 | --
1 | a = 1 | -- | <-- | a = 1 | 线程A拷贝主内存变量副本
2 | a = 3 | -- | -- | a = 1 | 线程A修改工作内存变量值
3 | a = 3 | -- | --> | a = 1 | 线程A工作内存变量存储到主内存变量,主内存变量还未更新
4.1 | a = 3 | a = 1 | <-- | a = 3 |线程B拷贝主内存变量副本随后主内存变量更新线程A工作内存变量
4.2 | a = 3 | a = 1 | <-- | a = 3 |线程A工作内存变量存储到主内存变量随后线程B获取主内存变量副本
操作4的时候可能出现:1.线程A变量值还未保存到主内存变量,2.线程A变量值保存到主内存变量。使用volatile关键字解决这个问题

public static volatile int a = 1; 特性

保证此变量对所有线程可见,一条线程修改的值,其他线程对新值可以立即得知

禁止指令重排序

可见性

修改内存变量后立刻同步到主内存中,其他的线程立刻得知得益于Java的先行发生原则

先行发生原则中的volatile原则:一个volatile变量的写操作先行于后面发生的这个变量的读操作,时间顺序

定义一个静态变量:static int a = 1;
线程A工作内存 | 线程B工作内存 | 指向 | 主内存 | 操作
-- | -- | -- | -- | --
-- | -- | -- | a = 1 | --
a = 1 | -- | <-- | a = 1 | 线程A拷贝主内存变量副本
a = 3 | -- | -- | a = 1 | 线程A修改工作内存变量值
a = 3 | -- | --> | a = 1 | 线程A工作内存变量存储到主内存变量
a = 3 | a = 3 | <-- | a = 3 | volatile原则:主内存变量保存线程A工作内存变量操作在线程B工作内存读取主内存变量操作之前

指令重排序和内存屏障

指令重排序:JVM在编译Java代码的时候或者CPU在执行JVM字节码的时候,对现有的指令进行重新排序,目的是为了再不影响最终结果的前提下,优化程序的执行效率

内存屏障:一种屏障指令,让CPU或比编译器对屏蔽指令之前和之后发出的内存操作执行一个排序约束。
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

非线程安全 public class VolatileTest implements Runnable { public static volatile int num; @Override public void run() { for (int i = 0; i < 1000; i++) { num++; } } public static void main(String[] args) { for(int i = 0; i < 100; i++) { VolatileTest t = new VolatileTest(); Thread t0 = new Thread(t); t0.start(); } System.out.println(num); } }

这段代码的结果有可能不是100000,有可能小于100000。
因为num++不是原子操作

使用原则

运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

变量不需要与其他的状态变量共同参与不变约束

三、原子性、可见性、有序性 原子性

一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

原子性保障

synchronized:monitorenter和monitorexit指令

atomic类型:底层为native方法

基本数据类型(long,double非原子协定除外)

可见性

当一个线程修改了共享变量的值,其他线程立即可知

可见性保障

volatile:先行发生(happens-before)原则

synchronized:对一个同步块unlock之前必须把工作内存变量同步到主内存中

final:final修饰的字段在构造器中初始化完成后,并且构造器没有把this引用传递出去,其他线程中就能看见final字段值

有序性

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/0949d8bd62668107e6daa4327bd42a69.html