这一章,我将结合JDK源码详细介绍如何通过AQS实现了读写锁以及如何通过Unsafe的park()
和unpark()
方法并结合条件队列实现Condition。
读写锁(ReentrantReadWriteLock)
1 读写锁的基本用法:
首先我们看一看读写的基本用法:
- 顾名思义,读写锁,就是能够保证读写分离,在高并发情况下,做到读读不互斥,读写互斥,写写互斥,从而提高并发量。
- 读写锁也是基于AQS实现的,通过AQS内部的state变量,同时记录的读线程的个数和写线程的个数(因为是可重入的,对于写线程的,state的值也可以大于1)
- 读写锁看似两个锁,其实只有一个锁。内部争用的都是同一个state变量。并且是基于同一个锁生成的
writeLock
和readLock
才具有读写锁的意义。
private static class SharedTask {
private int value;
private final ReadWriteLock lock;
private final Lock writeLock;
private final Lock readLock;
private SharedTask(int value) {
this.value = value;
// new 一个读写锁
this.lock = new ReentrantReadWriteLock();
// 获取写锁
this.writeLock = lock.writeLock();
// 获取读锁
this.readLock = lock.readLock();
}
public void write(int num){
try {
// 写方法,获取写锁
writeLock.lock();
value += num;
} finally {
writeLock.unlock();
}
}
public int read(){
try {
// 读方法 获取读锁
readLock.lock();
return value;
}finally {
readLock.unlock();
}
}
}
ReentrantReadWriteLock
读写锁的类继承层次关系
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
/** 内部类:写锁的实现,继承自Lock接口*/
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类:读锁的实现,继承自Lock接口*/
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 内部类:AQS的实现,继承自AbstractQueuedSynchronizer接口,是实现整个读写锁的具体对象:
有两个内部类实现,公平锁(FairSync)和非公平锁(NonfairSync)*/
final Sync sync;
......
}
3. 读写锁的实现原理
- 上文说道,ReadLock和WriteLock从表面上看,是两把不同的锁,但实际上它只是同一个锁的两个不同视图而已,即底层通过CAS争抢的同一个共享变量state。
- 读写锁的实现中,state变量被拆成两部分:
- 高16位:用于“读”锁,例如:高16位的值等于5,表示有5个读线程都拿到了该读锁,或者同一个读线程获取了5次读锁(可重入锁)
- 低16位:用于“写”锁,例如:低16位的值等于5,表示一个写线程获取了5次写锁(可重入锁),但是不可能存在5个写线程获取到写锁的情况(写写是互斥的)
- 当state==0时,说明没有线程占用锁,即没有读锁也没有写锁。当state!=0时,要么有读线程,要么有写线程持有锁,两者不可能同时成立,因为读写互斥。
为什么要把int变量state拆成两部分,而不是用两个int类型分别表示读锁和写锁的状态呢?
因为底层通过CAS实现,而无法通过一次CAS同时操作两个int变量,所以用来一个int型的高16位和低16位分别表示读锁和写锁的状态。
3.1. 首先我们来看读写锁的实现。
写锁是基于 AQS中的 acquire/release模板方法来实现lock/unlock的
读锁是基于AQS中的 acquireShared/releaseShared模板方法来实现lock/unlock的
// 读锁的实现
public static class ReadLock implements Lock, java.io.Serializable {
......
// 具体获取锁和释放锁的类:继承自AQS,由创建读写锁时传入:有公平和非公平两种不同实现
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
// 通过AQS中的acquireShared方法获取锁
sync.acquireShared(1);
}
public void unlock() {
// 通过AQS中的releaseShared方法获取锁
sync.releaseShared(1);
}
......
}
//写锁的实现
public static class WriteLock implements Lock, java.io.Serializable {
......
// 同上
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
// 通过AQS中的acquire方法获取锁
sync.acquire(1);
}
public void unlock() {
// 通过AQS中的release方法获取锁
sync.release(1);
}
......
}
3.2. AQS中的四种模板方法:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 获取写锁和互斥锁
public final void acquire(int arg) {
// tryAcquire()方法由子类Sync提供实现
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 释放写锁或互斥锁
public final boolean release(int arg) {
// tryRelease()方法由子类Sync提供实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 获取读锁,即可分享的锁
public final void acquireShared(int arg) {
// tryAcquireShared()方法由子类Sync提供实现
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 释放读锁,即可分享的锁
public final boolean releaseShared(int arg) {
// tryReleaseShared()方法由子类Sync提供实现
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
......
}
Sync有两个实现类:公平锁和非公平锁,因此将读/写,公平/非公平进行排列组合,便可以得到四种组合实现:
- 读锁的公平实现
- 读锁的非公平实现
- 写锁的公平实现
- 写锁的非公平实现
以上四种排列组合,通过Sync的两个子类FairSync和NonFairSync分别实现
// 非公平锁实现
static final class NonfairSync extends Sync {
// 写线程在抢锁时,是否应该阻塞:非公平,所以直接返回false
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 读线程在抢锁时,是否应该阻塞:非公平,所以判断当前阻塞队列的头结点是否为写线程,若为写线程,则不抢锁,进入阻塞状态。
// 为解决写线程一直处于“饥饿”状态的现象:
// 即当前持有锁的是读线程,如果读线程也能一直抢锁,则可能导致写线程永远拿不到锁。在读并发远远大于写并发时,很有可能出现。
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
// 公平锁的实现
static final class FairSync extends Sync {
// 写线程在抢锁时,是否应该阻塞:公平,所以需要判断当前阻塞队列是否有处于等待的节点
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// 读线程在抢锁时,是否应该阻塞:公平,所以需要判断当前阻塞队列是否有处于等待的节点
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
3.3 Sync抽象类对AQS中的tryAcquire/tryRelease方法的实现,即写锁(互斥锁)的实现。一行一行在代码中解读
// 写锁的获取流程
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取当前状态:即是否有读线程或者写线程获得锁
// c==0; 表示没有线程获取锁
// c!=0; 表示有读线程或者写线程获取锁(前面讲过,state变量被拆成高16位和低16位分别表示读锁和写锁的状态)
int c = getState();
// 通过位运算,获取写锁的状态
// w==0; 表示没有写线程
// w!=0; 表示有写线程,由于锁可重入,因此w可以大于1
int w = exclusiveCount(c);
// 有读线程或者写线程获取锁 进入if语句
if (c != 0) {
// w==0; 表示没有写线程,而c!=0;表示当前所持有线程,所以当前占有锁的一定是读线程,
// 而执行tryAcquire方法的是写线程,所以根据读写互斥原则,该线程需进入阻塞队列。返回false
// w!=0则说明持有锁的是写线程,而current != getExclusiveOwnerThread();即当前线程不是持有锁的线程:
// 根据写写互斥原则,该线程需进入阻塞队列。返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 执行到这里,说明持有锁的线程,就是当前执行tryAcquire的写线程。即w!=0&¤t===getExclusiveOwnerThread()
// 由于锁可重入,因此该线程可以重复获取锁,但需要校验重入次数不能超过最大值(即低16位所能表示的最大正数:65535)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 当前写线程获取到锁,直接更新state变量
// 执行到这里,说明当前线程一定是持有锁的线程,所以更新state变量是安全的,可以直接更新
setState(c + acquires);
// 成功获取到锁,返回true
// 注意:此处获取到锁,则说明一定是重入锁,所以无需设置持有锁的线程为当前线程,
// 即执行setExclusiveOwnerThread(current);
return true;
}
// writerShouldBlock(); 判断当前写线程是否应该阻塞,是则返回false,进入阻塞队列,否则通过CAS争抢锁(state变量)
// compareAndSetState(c, c + acquires);争抢锁,抢锁成功,则设置持有锁的线程为当前线程,否则返回false,进入阻塞队列
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置持有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
....
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 写锁的释放流程:tryRelease
protected final boolean tryRelease(int releases) {
// 判断持有锁的线程是否为当前线程,不是,则直接抛出异常,只有自己才能释放自己占用的锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取释放当前锁之后,state变量的值
int nextc = getState() - releases;
// 校验是否当前锁只有,state变量的值。
// 如果低16的值等于0,表示没有线程持有该锁,将持有锁的线程设置为null: setExclusiveOwnerThread(null);
// 如果低16位的值不等于0,则表示当前线程仍然有重入的锁,因此不更改持有锁的线程为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
// 更新state变量的值。
setState(nextc);
return free;
}
// 判断持有锁的线程是否为当前线程
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
3.4 Sync抽象类对AQS中的tryAcquireShared/tryReleaseShared方法的实现,即读锁(共享锁)的实现。一行一行在代码中解读
// 读锁的获取流程:tryAcquireShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// 获取当前state变量
int c = getState();
// exclusiveCount(c) != 0;表示当前持有锁的是写线程。
// 根据读写互斥原则,持有锁的线程不是当前线程时,是获取不到读锁的。所以直接返回-1.
// 此处注意:一个写线程获取到WriteLock后,可以再次获取ReadLock
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁的状态:即state高16位的值
int r = sharedCount(c);
// readerShouldBlock();读线程是否应该阻塞,公平锁与非公平锁的不同体现之处。
// r < MAX_COUNT; 获取读锁的线程个数(包括重入次数),要小于最大获取锁的个数(即16位正数,65535)
// compareAndSetState(c, c + SHARED_UNIT); 直接CAS抢读锁,高16位加1
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 进入if语句,说明该线程已获取到读锁。
// r==0; 说明当前线程是第一个获取到读锁的线程。
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// r!=0&&firstReader == current,说明第一个获取锁的线程再次获取到读锁(即重入),记录重入次数
firstReaderHoldCount++;
} else {
// r!=0&&firstReader != current; 说明其他读线程获取到读锁
// 以下代码用于统计各个获取到读锁的读线程的重入次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 执行到这里,说明当前读线程没有获取到锁。
// 通过一个for(;;)循环,自旋,不断的获取读锁,获取锁的逻辑和上面的逻辑差不多,不再过多赘述。读者可自行阅读源码。
return fullTryAcquireShared(current);
}
static final int SHARED_SHIFT = 16;
// 无符号右移16位,即表示获取到高16位的值
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
由于读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1,而是需要通过一个for循环+CAS操作不断重试。
// 读锁的释放流程:tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 判断当前线程是否是第一个获取读锁的线程
if (firstReader == current) {
// 第一个获取读锁的线程的重入次数等于1,则将firstReader设置为null
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 以下操作为更新当前线程的重入次数,如果重入次数小于等于1,则移除该线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 如果重入次数小于等于1,则移除该线程
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 由于会有多个读线程同时持有锁,因此释放时,也会有多个读线程同时释放锁,更新state变量。
// 因此state变量的更新不是安全,所以通过一个for循环+CAS重试的方式,更新state变量
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}