并发中关键字的语义
一、volatile的内存语义
1. 简单的举例
可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步;但是复合操作是不生效的;
package com.youyou.ch1.demo; public class Vola { volatile int a = 1; //使用volatile 声明int类型的变量 public int getA() { return a; //对单个volatile 变量进行读 } public void setA(int a) { this.a = a; //对单个volatile 变量进行写 } public void inc() { a++; //对复合(多个)volatile 变量进行 读/写 } }
这样的代用 volatile 关键字的代码和下面的是一样的;
package com.youyou.ch1.demo; public class VolaLikeSyn { int a = 0; //普通的变量 public synchronized int getA() { return a; //对单个普通 变量进行读 } public synchronized void setA(int a) { this.a = a; //对单个普通 变量进行写 } public void inc() //普通方法的调用 { int temp = getA(); //调用同步方法 temp = temp + 1; //普通的写操作 setA(temp); //调用同步的方法 } }
2. volatile变量自身具有下列特性:
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
2.1 volatile写的内存语义如下:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
2.2 volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
3. JMM对volatile的内存屏障插入策略
- 在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
4. volatile的实现原理
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。(这个Lock指令,使我们在反编译的时候也看不见是,是汇编层面的关键字)
将当前处理器缓存行的数据写回到系统内存 ,这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
二、锁的内存语义
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
synchronized的实现原理:
1. 使用monitorenter和monitorexit指令实现的
- monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
- 每个monitorenter必须有对应的monitorexit与之配对
- 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
- sychronized“方法”通常不是用monitorenter和monitorexit指令实现的。往往是由“方法调用指令”检查常数池里的ACC_SYCHRONIZED标志
2. 了解各种锁
锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
2.1 偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。无竞争时不需要进行CAS操作来加锁和解锁。
2.2 轻量级锁
无竞争时通过CAS操作来加锁和解锁。
2.3 重量级锁
三、final的内存语义
1. 编译器和处理器要遵守两个重排序规则。
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
2. final域为引用类型
- 增加了如下规则:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
3. final语义在处理器中的实现
- 会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
- 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障