以ReentrantLock为例,观察CAS基本使用。
class ReentrantLockExample {
int a = 0;
// 非公平锁
ReentrantLock lock = new ReentrantLock(false);
public void writer() {
// 获取锁
lock.lock();
try {
a++;
} finally {
// 释放锁
lock.unlock();
}
}
public void reader() {
// 获取锁
lock.lock();
try {
int i = a;
// ...
} finally {
// 释放锁
lock.unlock();
}
}
}
在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态
,这个volatile变量是ReentrantLock内存语义实现的关键。
ReentrantLock类图
使用公平锁时,加锁方法lock()调用轨迹如下:
在第4步真正开始加锁,源码如下:
protected final boolean tryAcquire(int acquires) { // acquires 调用时值为1
final Thread current = Thread.currentThread();
// 加锁前,先获取锁,读volatile变量(state初始值为0
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // CAS比较并交换
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;
}
}
lock()加锁前首先读volatile变量state。
在使用公平锁时,解锁方法unlock()调用轨迹如下:
在第3步真正开始释放锁,源码如下:
protected final boolean tryRelease(int releases) { // 调用时值为1
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 此时加完锁,state为1,1 - 1 = 0 nextc = 0
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
// 释放锁的最后,写volatile变量state,恢复为0
setState(nextc);
return free;
}
unlock()在释放锁的最后写volatile变量state。
非公平锁的释放和公平锁完全一样,仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下:
在第3步真正开始加锁,源码如下:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
该方法以原子操作的方式更新state变量(CAS)。
JDK文档对上述方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
此操作具有volatile读和写的内存语义。
编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序(插入内存屏障)。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
是一个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp。
这个本地方法的最终实现在openjdk的如下位置:openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于Windows操作系统,X86处理器)。intel X86处理器的源代码片段:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器
上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)
。反之,如果程序是在单处理器
上运行,就省略lock前缀
(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
intel的手册对lock前缀的说明:
确保对内存的读-改-写操作原子执行。
Intel使用缓存锁定(Cache Locking)
来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。(某些处理器可能会锁总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。)禁止该指令,与之前和之后的读和写指令重排序。
把写缓冲区中的所有数据刷新到内存中。
第2点和第3点所具有的内存屏障效果,足以让CAS同时实现volatile读和volatile写的内存语义。
公平锁和非公平锁的内存语义总结:
对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:
volatile变量
的写-读所具有的内存语义。CAS
所附带的volatile读和volatile写的内存语义。JVM中的CAS操作利用了处理器提供的CMPXCHG指令
实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
线程安全(原子类)/ 不安全 i++操作:
public class Counter {
private final AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
// 创建100个线程
Thread t = new Thread(() -> {
// 每个线程 数字 + 10000
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 期望 1000000
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println("consume time " +(System.currentTimeMillis() - start) + "ms");
}
/**
* 使用CAS实现线程安全计数器
*/
private void safeCount() {
// for (; ; ) {
// int i = atomicI.get();
// boolean suc = atomicI.compareAndSet(i, ++i);
// if (suc) {
// break;
// }
// }
// 两种都可以
atomicI.incrementAndGet();
}
/**
* 非线程安全计数器
*/
private void count() {
i++;
}
}
测试结果:
原子类可以保证线程安全操作。原子类虽好但也存在问题。
一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号
。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题
。
该类compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean compareAndSet(
V expectedReference, // 预期引用
V newReference, // 更新后的引用
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
)
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令
,那么效率会有一定的提升。
pause指令有两个作用:
第一,延迟流水线执行指令(de-pipeline)
,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
第二,避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率
。
相关CPU术语:
从Java 1.5开始,JDK提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。