本篇文章介绍volatile synchronized final 关键的基本概念以及的内存语义,通过自身的内存语义来理解Java内存模型具体如何通过限制编译器和处理器的重排序来为程序员提供内存可见性
volatile
volatile特性
可见性:对于一个volatile变量来说,总是能够看到任意线程对这个volatile变量最后的写入
volatile的可见性在线程A中有一个volatile的变量i,初始化后,对i写入数据i=1,这个时候线程A的本地内存的i变量会立即更新写入主内存中,当B线程需要读取i变量是,B线程的本地内存中的i变量副本会失效,迫使B线程的本地内存重新加载主内存中的i变量。这个就解释了volatile的可见性。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,当读取一个volatile变量是JMM会把该线程对应的本地内存中的共享变量置为无效,重新从主内存中加载共享变量。
原子性:对任意单个volatile变量的读写具有原子性,单类似于volatile++这种复合操作不具有原子性
对于Java中的double和long类型来说 他们都是64位,而对于32位的处理器来说,每次对double和long类型的变量进行写入操作的时候,会分两次进行,第一次写入前32位,第二次在写入后32位,这样在多线程的环境下,多个线程同时写入同一个double或者long类型的变量时,存在前32位和后32位可能出现不是同一个线程的写入。这样就造成了数据写入的混乱。而volatile关键字具有原子性,保证单个volatile编写的读写具有原子性,所以double或者long类型在多线程环境下可以使用volatile关键字来确保其原子性。
volatile内存语义
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
下面来看看volatile内存语义的实现过程
为了实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序。
volatile重排序规则表举个例子对于第三行最后一个单元格的NO解释:当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
综上所述
·当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
·当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
·当第一个操作是volatile写,第二个操作是volatile读时,不能重排序
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
内存屏障分类在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障
例子
上面步骤转换成指令后的执行顺序
synchronized
synchronized 关键字有二种使用方式
synchronized 代码块 synchronized方法
假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个
过程包含的happens-before关系可以分为3类。
1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happensbefore 6。
2)根据监视器锁规则,3 happens-before 4。
3)根据happens-before的传递性,2 happens-before 5。
synchronized 的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
所以锁的释放-获取和volatile的写-读具有相同的内存语义。volatile可以看过轻量级的锁,
final
对于final域,编译器和处理器要遵守两个重排序规则。
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
网友评论