和互斥锁相比,读写锁就是读线程和读线程之间可以不用互斥。
目录
前言
和互斥锁相比,读写锁就是读线程和读线程之间可以不用互斥。其实现为同一把锁的两个试图而已。可以理解为是一把锁,线程分为两类,读线程和写线程互斥,写线程和线程也互斥。共用同一个AQS。state变量拆成两半,一半表示写锁,一半表示读锁。
一、类继承层次
ReadWriteLock是一个接口,内部由两个Lock接口组成。
public interface ReadWriteLock {
//获取读锁
Lock readLock();
//获取写锁
Lock writeLock();
}
ReentrantReadWriteLock实现了该接口。当使用ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock。
二、读写锁实现的基本原理
从表面来看,ReadLock和WriteLock是两把锁,实际上它只是同一把锁的两个视图而已。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁),读线程和写线程互斥,写线程和写线程也互斥。
从ReentrantReadWriteLock 的构造方法中可知,readerLock 和 writerLock 实际共用同一个 sync 对象。sync 对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS。
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
同互斥锁一样,读写锁也是用 state 变量来表示锁状态的。只是 state 变量在这里的含义和互斥锁完全不同。在 ReentrantLock 中 同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,那针对一个 int 类型的同步状态它是怎么做到的呢?在读写锁中是通过“按位切割”的方式实现的,也就是把这个 int类型的变量切分为两部分,高16位表示读,低16位表示写。划分如下所示:
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 获取读锁数量
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁被重入的次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
这个地方的设计很巧妙,为什么要把一个 int 类型变量拆成两半,而不是用两个int型变量分别表示读锁和写锁的状态呢?这是因为无法用一次CAS 同时操作两个int变量,所以用了一个int型的高16位和低16位分别表示读锁和写锁的状态。当state=0时,说明既没有线程持有读锁,也没有线程持有写锁;当state!=0时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。
三、AQS的两对模板方法
acquire/release、acquireShared/releaseShard是AQS中的两对模板方法。互斥锁和读写锁的写锁都是基于acquire/release模板方法来实现的,读写锁的读锁是基于acquireShared/releaseShard这对模板方法来实现的。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
// 释放资源成功,tryRelease子类实现
if (tryRelease(arg)) {
// 获取头部线程节点
Node h = head;
// 首节点不为null,并且等待状态不为0
if (h != null && h.waitStatus != 0)
// 唤醒CLH队列队首节点对应的线程(首节点的下一个节点)
unparkSuccessor(h);
return true;
}
return false;
}
public final void acquireShared(int arg) {
/**
1. 负数表示失败
2. 0表示成功,但没有剩余可用资源
3. 正数表示成功且有剩余资源
*/
// <0表示获取资源失败
if (tryAcquireShared(arg) < 0)
// 自旋阻塞等待获取资源
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//释放资源成功,tryReleaseShared子类实现
//唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
对于公平模式下,读锁和写锁一样,只要队列中有其他线程在排队,就不能直接去抢锁,要排在队列尾部。
对于非公平模式下,写锁在state=0时会去抢锁。读线程在state=0且队列中第一个线程不是写锁时去抢锁。降低写线程饥饿的概率,但还是可能导致写线程饥饿。
四、WriteLock公平和非公平实现
写锁时排他锁,实现策略类似于互斥锁。
tryAcquire实现分析
// Sync 实现的 AbstractQueuedSynchronizer 中获取独占锁的方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 获取当前同步状态
int w = exclusiveCount(c); // 获取写锁重入数量
if (c != 0) { // 如果当前同步状态不为 0,表示锁(读锁或写锁)已被占用
// 如果 w == 0 说明存在读锁,直接返回,为了保证写对读可见,当前写线程必须阻塞。
// 如果 w != 0 说明存在写锁,判断当前线程不是已获取写锁的线程,获取锁失败,当前线程被阻塞
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果低 16位满了表示超过了获取锁数量的最大值抛出异常
throw new Error("Maximum lock count exceeded");
// 自己重复获得锁,直接修改state
setState(c + acquires);
return true;
}
if (writerShouldBlock() || // 此方法是判断抢锁前是否被阻塞。非公平锁永远不阻塞,公平锁如果前面有其他线程在排队就要阻塞
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current); //如果抢锁成功,把持有锁的线程更新为当前线程
return true;
}
上面的代码分析如下:
1.if(c!=0) and w == 0,说明当前一定是读线程拿着锁,写锁一定拿不到,返回false。
2.if(c!=0) and w != 0,说明当前一定是写线程拿着锁,执行current!=getExclusiveOwnerThread()的判断,发现ownerThread不是自己,返回false。
3.c!=0,w!=0,且current=getExclusiveOwnerThread(),才会走到if(w+exclusiveCount(acquires)> MAX_COUNT)。判断重入次数,重入次数超过最大值,抛出异常。因为是用state的低16位保存写锁重入次数的,所以MAX_COUNT是216。如果超出这个值,会写到读锁的高16位上。为了避免这种情形,这里做了一个检测。当然,一般不可能重入这么多次。
4.if(c==0),说明当前既没有读线程,也没有写线程持有该锁。可以通过CAS操作开抢了。
公平实现和非公平实现几乎一模一样,只是 writerShouldBlock() 分别被FairSync 和NonfairSync实现,公平锁会判断是否有在等待的写锁,而非公平锁则会直接抢锁。
tryRelease实现分析
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively()) //如果当前线程没有获取锁则抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc); // 因为写锁为排它锁,不会存在其他线程占有写锁或读锁,所有不用使用 CAS操作
return free;
}
五、ReadLock公平和非公平实现
读锁是共享锁。
tryAcquireShared实现分析
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && //写锁的数量不为0
getExclusiveOwnerThread() != current) // 持锁线程不是当前线程,因为写锁也可以获得读锁
return -1;
int r = sharedCount(c); // 获取读锁的状态
if (!readerShouldBlock() && // 判断当前线程是否需要阻塞等待。
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { // 如果读锁未被获取过,则更新第一个读线程为当前线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 如果第一个读线程为当前线程则数量加1
firstReaderHoldCount++;
} else {
// 获取读锁的线程重入的次数是保存在 ThreadLocal 中的有线程本身保存
// 着自己获取锁的次数
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;
}
return fullTryAcquireShared(current); // 如果上面拿锁失败,则进行自旋不停的拿锁
}
tryReleaseShared实现分析
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) { // 通过自旋 + CAS 的方式更新读锁状态
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接-1,而是需要自旋CAS的方式不断重试。这是tryReleaseShared和tryRelease的根本差异所在。
锁降级
锁降级是指写锁可以降级为读锁。如果当前线程拥有写锁,在不释放写锁的情况下是可以在获取读锁的,完后在释放(先前获取)的写锁。ReentrantReadWriteLock 是支持锁降级,但不支持锁升级。锁升级会导致死锁问题。