1.volatile内存语义
Volatile主要作用是使变量在多个线程间可见
1.1 volatile特性
- 可见性:对一个volatile变量的读,总能看到(任意线程)对该变量最后的写入
- 原子性:即使是64为的long型和double型变量,只要声明为volatile变量,对该变量的读写就具有原子性volatile变量的复合操作不具有原子性,如volatile++
1.2 volatile写-读的内存语义
volatile写和锁的释放有相同的内存语义,volatile的读和锁的获取有相同的内存语义
volatile写的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量的值刷新到主存
volatile读的内存语义:
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存读取共享变量
1.3 volatile内存语义的实现
为了实现volatile语义,JMM会限制重排序(编译器,处理器),volatile重排序规则表如下(无数据依赖性):
-
不管volatile读后面的操作是啥,都不能重排序
-
不管volatile写前面的操作是啥,都不能重排序
-
volatile读写不管顺序如何都不能重排序
JMM保守实现策略(可根据不同处理器优化):
序号 | 位置 | 插入屏障 |
---|---|---|
1 | 每个volatile写前 | 写写(Store-Store)屏障 |
2 | 每个volatile写后 | 写读屏障 |
3 | 每个volatile读后 | 读读屏障,读写屏障 |
第二条可以替换为每个volatile读前插入,但是这样替换会导致效率变低(一写多读)
实际执行时,只要不改变volatile写-读的内存语义,可以省略一些不必要的屏障
X86处理器仅会对写读重排序,所以JMM只需要在最后一个volatile写之后插入写-读屏障,其余屏障都会省略,所以x86处理器中volatile写的开销比读的开销大
1.4 volatile内存语义增强
JSR-133(jdk1.5)前允许volatile变量操作和普通变量操作重排序,无法保证数据的安全性
JSR-133后禁止了这种排序,确保了volatile的写-读和锁的释-放获取具有相同的内存语义
2. 锁的内存语义
volatile仅仅保证单个volatile变量的读写具有原子性,锁的互斥特性可以保证整个临界区代码的执行具有原子性
功能上,锁更加强大;在可伸缩性和性能上,volatile更具有优势。
2.1 锁的释放和获取的内存语义
当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主存中
当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从主存中读取共享变量
A线程释放锁,B线程获取同一个锁,相当于A线程通过主存向B线程发送消息
2.2 锁内存语义的实现
ReetrantLock类图(部分)如下:
AQS:AbstractQueueSynchronized,Java同步器框架
公平锁,线程获取锁顺序按照线程加锁的顺序来分配
非公平锁,获取锁抢占机制,随机获取锁
2.2.1 公平锁加锁
公平锁加锁调用轨迹如下:
- ReentrantLock:lock()
- FairSync:lock()
- AQS:acquire(int arg)
- ReentrantLock:tryAcquire(int acquires),真正开始加锁
tryAcquire方法部分代码如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //公平锁加锁方法首先获取volatile变量
//其他操作
return;
}
2.2.2 解锁(公平/非公平)
解锁unlock的调用轨迹如下:
- ReentrantLock:unlock()
- AQS:release(int arg)
- Sync:tryRelease(int releases)
tryRelease方法部分代码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//其他操作
setState(c); //解锁的最后写volatile变量
return ;
}
公平锁的获取和释放通过操作volatile变量实现
编译器和处理器不会对volatile写与写之前,volatile读与读之后的代码重排序
2.2.3 非公平锁加锁
非公平锁加锁调用轨迹如下:
- ReentrantLock:lock()
- NonfairSync:lock()
- AQS:compareAndSetState(int expect,int update)
compareAndSetState方法代码如下:
protected final boolean compareAndSetState(int expect, int update) { //CAS操作
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
当状态值等于预期值,则以原子方式将同步状态设置为给定的更新值,CAS同时具有volatile读和写的内存语义
2.2.4 CAS内存语义
编译器和处理器不能对CAS和CAS前面或后面的任意内存操作重排序,同时具有volatile读和写的内存语义
程序会根据当前处理器的类型决定是否为cmpxchg指令添加lock前缀:
- 多处理器,加上lock前缀,(Lock Cmpxchg)
- 单处理器,省略lock前缀(单处理器具有顺序一致性)
lock前缀的作用如下:
- 确保对内存读-改-写操作的原子性,一些处理器会使用总线锁定,目前更多的使用缓存锁定
- 禁止该指令和之前之后的读和写指令重排序
- 把写缓冲区的所有数据刷新到内存中
2.2.5 总结
- 公平锁和非公平锁释放,最后都要写一个volatile变量
- 公平锁获取时,首先会去读volatile变量
- 非公平锁获取时,首先会用CAS更新volatile变量
锁的内存语义实现至少有两种方式:
- 利用volatile变量的写-读所具有的内存语义
- 利用CAS所附带的volatile读和volatile写的内存语义
2.3 concurrent包的实现
CAS同时具有volatile读和写的语义,因此线程有四种通信方式:
- A线程写volatile变量,B线程随后读这个volatile变量
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
JUC包的通用实现方式:
- 声明变量为volatile
- 使用CAS的原子条件更新来实现线程之间的同步
- 配合volatile的读/写和CAS所具有的volatile读写的语义实现线程的通信
实现示意图:
3. final域的内存语义
与锁和volatile相比,对final域的读写更像是普通的变量访问
3.1 final域重排序规则(语义)
-
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
-
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
3.2 写final域重排序规则
禁止把final域的写重排序到构造函数之外:
- 编译器:JMM禁止编译器把final域的写重排序到构造函数外
- 处理器:编译器会在构造函数return之前插入一个写-写屏障来禁止处理器把final域的写重排序到构造函数外
写final域重排序可以确保——在对象引用对任意线程可见之前,对象的final域已经被正确初始化过了,普通域没有这个保证
3.3 读final域重排序规则
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM会禁止重排序这两种操作
读final域重排序可以确保——在读一个对象的final域之前,一定会先读包含这个final域的对象的引用
3.4 final域为引用类型
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作间不能重排序
public class Example{
final int[] array;
static Example obj;
public Example(){
array =new int[1];array[0]=1; //操作1
}
public static void write(){
obj=new Example(); //操作2
}
//其他操作
}
3.5 final引用不能“溢出”
在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“溢出”
public class Example2{
final int i;
static Example2 obj;
public Example2(){
i=1;
obj=this; //这一步会重排序导致“溢出”
}
//其他操作
}
3.6 final语义的实现
(编译器)在final写之后,构造函数return之前插入一个写写(StoreStore)屏障
在读final域的操作前面插入一个读读(LoadLoad)屏障
X86处理器中,final域的读/写不会插入任何内存屏障,X86可以保证这些操作不重排序
- 写写不会重排序(写内存语义)
- 有间接数据依赖的不会重排序(读内存语义)
3.7 final增强语义
JSR-133增强了final语义,提供了初始化安全保证:
只要对象是正确构造的(无逸出),那么不需要使用同步就可以保证在任意线程都能看到这个final域在构造函数中被初始化之后的值