volatile内存语义:
通过lock前缀对多处理器发出指令。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量的值刷新到主内存;当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,然后线程将从主内存中读取共享变量。
volatile内存语义的实现:
内存屏障的插入可以禁止编译器的重排序,且JMM采取保守策略,保证任意处理器平台的任意程序都可以得到正确的volatile语义。策略如下:
1)在每个volatile写操作的前后分别加上StoreStore屏障和StoreLoad屏障。StoreStore屏障将volatile写之前的普通写刷新到主内存,StoreLoad屏障避免当前的volatile写与后面可能的volatile读/写操作重排序。也就是说,编译器不会对volatile写和volatile写前面的任意内存操作重排序。
2)在每个volatile读操作的后面依次加上LoadLoad屏障和LoadStore屏障。LoadLoad屏障用来禁止volatile读与后面的普通读重排序,LoadStore屏障用来禁止volatile写与后面的普通写重排序。也就是说,编译器不会对volatile读和volatile读后面的任意内存操作重排序。
锁的内存语义:
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程获取锁时,JMM会把该线程对应的本地内存置为无效,然后线程将从主内存中读取共享变量(和volatile内存语义几乎一样发现了没~)。
锁内存语义的实现:
以ReentrantLock为例,ReentrantLock有两个内部类的锁,为公平锁和非公平锁。以公平锁为例。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
由公平锁源码可知,加锁方法首先是通过getState()方法读取volatile变量state的值(这里使用了volatile读的内存语义,即编译器不会对volatile读和volatile读后面的内存操作重排序),然后再通过state的值判断这个锁是否被占用,在判断时,调用了一个AQS抽象类下的compareAndSetState()方法,也就是unsafe类下的compareAndSwapInt()方法,该方法简称以原子操作的方式更新int类型的state的值,该类方法简称CAS。JDK文档中对该类方法的说明为:如果当前值等于预期值,则以原子方式将同步状态设置为给定的更新值。直译该方法,也就是原子操作类型的比较交换,具有volatile读和volatile写的内存语义,编译器不能对CAS和CAS前后的任意内存操作重排序。
而解锁的话,依旧是首先读这个volatile变量state,如果是可重入锁的话,state的值可以是0-n;如果是非重入锁的话,state的值是0或1。一般来说,执行一次tryRelease()方法就是释放一次锁,也就是state的值减1,只有当state的值重新为0的时候,才代表锁能够被释放。然后在解锁方法的最后来写新的volatile state变量(这里使用了volatile写的内存语义,即编译器不会对volatile写和volatile写前面的内存操作重排序),并使后续获取锁的线程读取该volatile变量后立即变得对获取锁的线程可见。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
至于为什么能具有volatile读/写的内存语义,如下可以看到,这是一个native方法,是通过C++实现的。大致就是说,如果程序是在多处理器上运行,该native方法会为指令加上lock前缀,提供内存屏障效果;反之省略。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
concurrent包的实现
如果仔细分析concurrent包的源代码,会发现一个通用化的实现模式。以AtomicInteger为例:
1)声明共享变量为volatile;
private volatile int value;
2)使用CAS的原子条件更新来实现线程之间的同步,随便举AtomicInteger类中的几个修改值的方法可以看到,都是通过调用unsafe类中的CAS方法来对volatile变量进行更新;
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
3)配合volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
基于volatile的双重检查锁定
有时候需要采用延迟初始化来降低初始化类和创建对象的开销,只有在使用该对象时才进行初始化。原理是通过static关键字的特性,在加载类时,就初始化static修饰的成员变量,然后在调用双重检查锁定方法的时候,才实例化一个对象并让初始化的引用指向该对象,也就是完成对象的初始化。
在这里,volatile关键字的作用是防止重排序,避免方法返回一个并没有完成初始化的对象。
public class Test {
private volatile static Child1 instance;
public static Child1 getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null) {
instance = new Child1();
}
}
}
return instance;
}
}
谈一谈volatile关键字的一个弊端
先说一说CPU缓存一致性MESI协议。要解决缓存不一致问题,只有通过总线lock和缓存一致性协议两种方式,但通过锁住总线是一种效率比较低下的方式,所以出现了缓存一致性协议。缓存一致性协议中最出名的就是MESI协议,该协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
那么问题来了:CPU和内存的交互,是要通过三级缓存的,而交互的最小单元,就是缓存行。通常一个缓存行为64字节,哪怕读取4字节数据,也会连续读取该数据之后的60字节。也就是说,如果通过volatile读读取了一个缓存行,且只有前4个字节才是真正需要读取的对象,就会造成性能损耗上的问题。因为最小操作单元是一个缓存行,所以volatile关键字锁住了一整个缓存行的对象,其它线程就都无法再对多余的60个字节的对象做操作。解决办法也很简单,就是通过填充空白字节使对象膨胀,使无关的共享变量放在不同的缓存行中,也就是牺牲空间换时间。
final域的内存语义:
对于final域,编译器和处理器要遵守两个重排序规则:
1)在构造函数内写入一个final域,和随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序(通过在构造函数return之前,插入一个StoreStore屏障,禁止处理器把final域的写重排序到构造函数之外,以保证能够正确的读取到final变量初始化之后的值);
2)初次读一个包含final域的对象的引用,和随后初次读这个final域,这两个操作之间不能重排序(通过在读final域之前插入一个LoadLoad屏障,禁止处理器把final域的读重排序到读对象引用之前,以保证在读一个对象的final域之前,一定会先读包含这个final域的对象的引用)。