在前三篇文章中,我们已经考察了同步队列与条件队列的创建,入队,出队,取消,与转移过程;这一篇文章,我们来考察AQS的应用;
文章目录
ReentrantLock
可重入锁与AQS都位于java.util.concurrent.locks包下,可以说是AQS最近的应用;
如何创建一个重入锁对象
Sync是AQS的子类,同时又是FairSync与NonfairSync的父类,我们从创建一个重入锁对象来开始分析;
ReentrantLock提供两个构造器,默认的无参构造器实例化一个非公平的同步器;有参构造器,根据传入的值构造同步器;
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如何实现锁定
是如何实现锁的?AQS的state是共享变量;当state的值=0时,表示资源未被占用;state>0时表示资源已被占用,实现锁定效果;
/**
* Acquires the lock.
*
* <p>Acquires the lock if it is not held by another thread and returns
* 获取锁如果不被另一个线程占用则快速返回
* immediately, setting the lock hold count to one.
*并设置锁的占有次数=1
* <p>If the current thread already holds the lock then the hold
* 如果当前线程已经持有了锁,则持有锁的次数+1并立即返回(重入)
* count is incremented by one and the method returns immediately.
*
* <p>If the lock is held by another thread then the
* 如果锁被其余线程持有
* current thread becomes disabled for thread scheduling
* 当前线程不可被线程调度
* purposes and lies dormant until the lock has been acquired,
* 并且处于休眠直到锁被获取到
* at which time the lock hold count is set to one.
* 在这个时候,锁的持有数量被设置为1
*/
public void lock() {
sync.lock();
}
根据方法的签名与注释,我们知道这个锁不响应中断。
继续追踪代码,有公平锁与非公平锁两种实现;
公平锁与非公平锁的区别
公平锁实现
final void lock() {
acquire(1);
}
非公平锁实现
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平与非公平区别点1:
在于在与非公平锁在常规的获取资源方法之前,优先cas设置资源从0->1,如果失败再进行常规获取资源操作。
独占,与重入
acquire是一个模板方法,需要调用子类实现的tryAcquire方法;
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* 独占模式下获取,忽略中断。
* by invoking at least once {@link #tryAcquire},
* 通过至少调用一次tryAcquire实现
* returning on success. Otherwise the thread is queued, possibly
* 当成功时返回。不然线程入队,
* repeatedly blocking and unblocking, invoking {@link
* 可能重复的阻塞与非阻赛,
* #tryAcquire} until success. This method can be used
* 知道获取资源成功
* to implement method {@link Lock#lock}.
* 这个方法可以用来实现锁定
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
// 尝试获取资源失败,成功,则返回
// 否则,入队;直到在队列中获取到资源后才可以返回;如果有发生中断,则设置中断信息
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued我们研究过,我们现在来研究 tryAcquire 方法;
公平实现
/**
* 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();
// 资源为0表示未被锁定
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");
// 重入次数,设置state的数量增加,表示重入次数
setState(nextc);
return true;
}
// 资源为0时自己无法竞争或者竞争失败,或者资源已被锁定,则直接返回false;
return false;
}
}
非公平实现
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* 实施非公平尝试锁。tryLock也是非公平的
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 设置独占
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平与非公平区别点2:
与公平锁的区别在于,当检测到资源为0时,直接尝试CAS锁定,不检测队列中是否有其他的排队线程;
总结
我们已经分析了重入锁如何实现锁,实现重入,以及考察了公平与非公平的区别;区别在于,当调用lock方法时,公平锁会先检查当前资源是否可以占有,再检查队列中是否有别的节点在,或者自己是否位于队首,才去CAS锁;而非公平锁,则是直接尝试cas锁定资源,cas失败后检测到资源还未占用,再去cas锁定一次;
如何释放锁
/**
* Attempts to release this lock.
* 尝试释放锁
* <p>If the current thread is the holder of this lock then the hold
* 如果当前线程持有锁则持有数量-1;
* count is decremented. If the hold count is now zero then the lock
* 如果锁持有数量为0,则锁释放
* is released. If the current thread is not the holder of this
* 非持有锁线程无法释放锁,抛出非法管程状态异常
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
* hold this lock
*/
public void unlock() {
sync.release(1);
}
/**
* Releases in exclusive mode. Implemented by unblocking one or
* 独占模式下释放
* more threads if {@link #tryRelease} returns true.
* 调用tryRelease解锁一个或多个线程
* This method can be used to implement method {@link Lock#unlock}.
* 该方法可以被用来实现解锁
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
// 子类实现的tryRlease方法,释放资源
if (tryRelease(arg)) {
// 获取同步队列头节点
Node h = head;
// 如果头节点不为空,且头节点状态不为0,因为尾插法插入同步队列后,都会将前置节点置为-1,自己才会park;为0表示没有后继节点
if (h != null && h.waitStatus != 0)
// 唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
// 不区分公平或者非公平,尝试释释放资源
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;
}
独占模式下,唤醒后继者
在释放资源成功后,检查头节点的状态是否为非0,是非0则唤醒后继者;
何时头节点为0
在这里,我们考察一下,何种场景下头节点为0;
- 1,队列创建时,头部节点状态默认为0;入队的节点立刻获取到资源,将自己设置为头节点;当没有额外的资源入队时,此时头节点为0;
- 2,剩余的任何调用acquire的方式,在阻塞自己之前都会设置头节点为-1;故任何调用acquire方法的入队阻塞了的节点,都会使得头部节点为-1;
- 3,调用acquire时入队,但阻塞自己之前发生错误,尚未设置为-1,此时头节点可能时0;在取消获取方法中cancelAcquire,我们分析过,三种场景,自己是尾节点,那么头节点将会称为尾节点,符合1;自己的前置节点是头节点,再唤醒自己节点的后续节点,当唤醒自己的后继节点时;如果有可以唤醒的后继节点,则会唤醒它,没有的话头节点是0,符合1;唤醒后的节点获取资源失败,会再次尝试更改头节点为-1,才会park,符合2;最后一种场景,即自己不是尾,也不是头节点的下一个节点,则会设置头节点的状态为-1;设置失败则会park后继节点;总结下来,就是取消的场景,也会保证只要有合理的后继节点,头节点就是-1,否则才为0;
- 4,从条件队列同步入队;正常唤醒的 transferForSignal方法在入队后,会尝试设置前置节点状态为-1,设置失败则会唤醒该线程,而该线程阻塞在await方法处,线程被唤醒后发现自己无法获取资源又会在沉睡前设置前置线程为-1;唤醒成功,则头节点为当前节点,即状态为-2;小于0;
- 5,最后一种情况就是条件队列取消等待后进入到同步队列,transferAfterCancelledWait;如果未能将节点状态改为0,则会通过signal方法入队;而如果成功修改为0;则会尝试从队列中获取资源 acquireQueued,成功则会将自己置为头节点;失败则会将前置节点状态为-1;实际上只要自己成功入队,且后续节点还有节点,就一定不会让头节点为0
总结下来,只要头节点还有后续节点,头节点就为-1或者-2;而不会为0;为0的时候,都是没有后续节点了;
独占模式唤醒后继者源码分析
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* 如果当前节点的状态为负,尝试将它置为0
* to clear in anticipation of signalling. It is OK if this
* 当置为0失败时,表示其余的线程已经将它更该,比如改为-1
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* 通常情况下,只需要唤醒后继节点;当后继节点取消或者是看起来是null
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* 后续遍历直到找到一个非取消的节点;
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
关于从后向前遍历的原因,参阅AQS后续遍历找寻继任者
总结
我们分析了AQS作为独占,可重入锁,可选公平与非公平是怎么实现与解锁的,还有可中断锁我们没有分析,是因为可中断锁只是AQS的实现,在 acquire 方法中将中断信号当作异常抛出;另外,还有一些实用的方法,我们没有分析,比如判断锁是否被当前线程占有,当前锁的排队情况等;
总的来说,ReentrantLock也是利用管程思想实现了的AQS锁。
ReentrantReadWriteLock
读写锁简介
可重入读写锁在重入锁基础上升级,在读多写少等场景下,有更好的并发性能;从预期上,我们希望读读之间可以并发,写的时候不允许读。至于读的时候是否允许写,ReentrantReadWriteLock 是不允许读写并发,为了防止脏读。读的时候不允许写,但读读之间共享,可能会导致写线程无限等待,造成饥饿。我们就带着这个问题,去寻找是否有对应的解决方案,来分析可重入读写锁。
读写锁类结构
在上述类结构中,我们看到ReentrantReadWriteLock 实现了 ReadWriteLock 接口,内部有ReadLock与WriteLock两个类作为具体的读写锁。还有我们熟悉的的三个子类,Sync,FairSync,NonfairSync;我们先从熟悉的公平与非公平的可选同步器开始。
公平与非公平同步器
/*
* Acquires and releases use the same code for fair and
* 公平锁与非公平锁,获取与释放使用相同的代码
* nonfair locks, but differ in whether/how they allow barging
* 不同之处在于当队列非空时它们是否/如何插入
* when queues are non-empty.
*/
/**
* Fair version of Sync
* 公平版本的同步器
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
/**
* Nonfair version of Sync
* 非公平版本的同步器
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
// 非公平写锁,无论如何都不应阻塞
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* 避免无限期写线程饥饿的试探方法,
* block if the thread that momentarily appears to be head
* 阻塞如果队列第一个元素是一个写等待线程
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* 这只是一种概率效应,因为在写等待线程之前还有其他排队的读等待线程,
* block if there is a waiting writer behind other enabled
* 新来的读线程不会被阻塞
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Returns {@code true} if the apparent first queued thread, if one
* 如果队首元素存在且时独占模式返回true
* exists, is waiting in exclusive mode. If this method returns
* 如果这个方法返回true
* {@code true}, and the current thread is attempting to acquire in
* 且当前线程在共享模式下获取
* shared mode (that is, this method is invoked from {@link
* #tryAcquireShared}) then it is guaranteed that the current thread
* 那么保证当前线程不是第一个队列中的线程。
* is not the first queued thread. Used only as a heuristic in
* 该方法是仅在读写锁中作为试探法使用
* ReentrantReadWriteLock.
*/
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
可以看出,公平与非公平的区别在于当队列中有其他元素时,公平同步器总是阻塞自己,而非公平的写锁则是不阻塞,而非公平的读锁则是判断如果队首元素是否是写等待线程。
在这里我们可以看出,doug lee 大神在非公平的读锁时,为了减少阻塞的队列。
我们直到非公平锁的效率高于公平锁,但是非公平写锁可能会产生饥饿,故对非公平读锁加了一点点限制,就是当队首为写线程时,则阻塞非公平读锁。
读锁与写锁的实现
如何构造一个读锁/写锁
ReentrantReadWriteLock 的构造器,在这个构造器中根据是否指定公平锁,创建一个对应的同步器,并分别去构造对应的读锁与写锁。
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// 同步器初始化方案
Sync() {
readHolds = new ThreadLocalHoldCounter();
// 此时state为0,对volatile字段进行读写操作,禁止重排序并保证可见性
setState(getState()); // ensures visibility of readHolds
}
/**
* Constructor for use by subclasses
* 构造一个读锁
* @param lock the outer lock object
* @throws NullPointerException if the lock is null
*/
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
/**
* Constructor for use by subclasses
* 构造一个写锁
* @param lock the outer lock object
* @throws NullPointerException if the lock is null
*/
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
读写锁如何实现锁定与解锁的背后原理
我们已经直到AQS提供独占模式的获取与释放,而且写锁就是独占模式,自然而然的写锁的原理就是AQS的独占模式下的获取与释放;而读锁是共享的,自然而然读锁的原理就是AQS的共享模式下的获取与释放。
WriteLock
public void lock() {
sync.acquire(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public void unlock() {
sync.release(1);
}
/**
* Performs tryLock for write, enabling barging in both modes.
* 写锁的tryLock,在两种模式中执行插入(公平与非公平)
* This is identical in effect to tryAcquire except for lack
* 这与tryAcquire效果相同只是缺少检查 writerShouldBlock
* of calls to writerShouldBlock.
*/
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
// 共有的资源,低16位被用来标识写锁的个数,因为写锁是独占锁,只要写锁自增+1就代表重入
int c = getState();
// 资源被占用
if (c != 0) {
// 写锁的个数
int w = exclusiveCount(c);
// 资源被占用,但是写锁为0,表示有读锁,有读锁时不能获取写锁,故直接返回失败
// 资源被占用,且写锁不为0,检查写锁的持有线程是不是当前线程,不是当前线程则返回失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 资源被占用,且时当前线程占用的,检查是否达到写锁上限个数,1<<16-1的个数
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
// 资源未被占用,则尝试直接占用,占用失败则直接返回失败
if (!compareAndSetState(c, c + 1))
return false;
// 再次设置当前线程为独占锁拥有线程
setExclusiveOwnerThread(current);
return true;
}
ReadLock
public void lock() {
sync.acquireShared(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public void unlock() {
sync.releaseShared(1);
}
/**
* Performs tryLock for read, enabling barging in both modes.
* 读锁的tryLock,在两种模式下都允许插入(公平与非公平)
* This is identical in effect to tryAcquireShared except for
* 与 tryAcquireShared 效果一致,仅是缺少readerShouldBlock检查
* lack of calls to readerShouldBlock.
*/
final boolean tryReadLock() {
Thread current = Thread.currentThread();
// 循环获取确切结果
for (;;) {
// 产看资源个数
int c = getState();
// 独占锁不为0且独占线程不是当前线程,则直接失败;写读不并发
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
// 读锁数量
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 将锁定的资源个数设为c+1<<16,因为c的高16位标识读锁的个数
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果读锁数量为0
if (r == 0) {
// 当前线程赋值第一个读锁线程
firstReader = current;
//第一个读锁线程持有读锁数量为1 实现重入
firstReaderHoldCount = 1;
// 读锁数量不为1,且当前线程为第一个读锁线程
} else if (firstReader == current) {
// 第一个读锁线程持有锁数量+1
firstReaderHoldCount++;
} else {
// 当前线程不是第一个读锁线程,则记录当前线程持有读锁的数量,实现重入
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 true;
}
}
}
读写锁如何实现重入计数的
在上一节的tryWriteLock与tryReadLock中,我们已经看到过相关关于重入计数。
ReentrantReadWriteLock 将 AQS的int 类型 state 字段,分为高16位 unsined short标识读锁的数量,而低16位表示写锁的数量;
由于写锁是独占锁,故写锁的重入只需要将state字段进行加减即可;
读锁是共享锁,不能再简单的对state字段进行加减实现。对于第一个读锁线程,用 Thread firstReader 表示这个线程,用 firstReaderHoldCount 表示这个线程重入的次数;对于其余的读线程,使用 HoldCounter 记录线程id 与线程的重入次数,存储在 hreadLocalHoldCounter readHolds 中;这样总的读锁数量在 state的高16位标识,而每个线程的持有锁数量在每个线程自己的houldCounter中。
读锁与写锁阻塞与释放
我们已经了解过公平与非公平,读锁与写锁怎么实现的(基于 直接返回结果的 tryReandLock和tryWriteLock),以及如何实现重入的。现在我们完整分析Sync是如何读锁与写锁的。
写锁获取与 tryAcquire
我们已经直到acquire是模板方法,直接调用子类的tryAcquire方法失败后则尝试入队以及再在queuedAcquire方法中tryAcquire实现获取资源或park;
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 如果读锁非0且非同一个线程,失败
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 如果超过最大数量,失败
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
* 线程要么是独占重入或者队列允许独占而获取到独占资格,则更新资源并设置独占线程
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 重入获取
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 当不是重入获取时,应检查是否阻塞当前写线程(公平锁是检查队列,非公平是非阻塞)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
可以看出与 tryWriteLock的唯一区别就是 writerShouldBlock 检测;这也是公平与非公平子类唯一的工作;
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);
return free;
}
写锁,是独占锁,独占锁的释放我们已经很熟悉了。
读锁与 tryAcquireShared
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 读锁与写锁互斥,如果写锁被其他线程占有,无法获取读锁
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 当前线程有资格获取读锁(队列中的首元素不是写等待线程)
* 尝试cas锁定并更新读锁数量,这一步尚未检测重入场景;比如虽然不允许我们获取读锁,但是我们曾经是第一个获取过读锁的线程,或者是我们曾经获取过读锁,且仍然保持着读锁,即时此时队列中已经有排队的节点无论是否写线程或者读线程
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
* 如果应该阻塞或者cas失败或者读数量饱和则进行循环版本完整的 fullTryAcquireShared
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 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) {
firstReaderHoldCount++;
} else {
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);
}
/**
* Full version of acquire for reads, that handles CAS misses
* 完整版本获取读锁,这里处理cas失败和重入读
* and reentrant reads not dealt with in tryAcquireShared.
*/
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// 如果我们自己持有排他锁,阻塞在这里会导致死锁;即锁降级需要
// would cause deadlock.
// 写锁为 0 但 需要阻塞的场景
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
// 我们是否是第一个已读线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 在应该阻塞的场景下,且我们不是第一个已读线程,或者尚未有已读线程
if (rh == null) {
rh = cachedHoldCounter;
// 获取当先线程的计数器
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
// 当前线程计数器如果为0则清除
readHolds.remove();
}
}
// 判断当前线是否持有过读锁
if (rh.count == 0)
return -1;
}
}
// 最大读锁量检查
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 此时,已经允许我们获取锁,cas操作,失败则继续循环
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
tryAcquireShared 与 tryReadLock逻辑一致,只是多了 shouldBlock检查,以及对重入的允许;在这里我们可以看到,可以在持有写锁的同时,本线程在获取读锁。
在写锁中获取读锁
在Doug Lee 给的例子中,获取写锁完毕,再释放写锁之前,先获取读锁;
这样做的原因是防止脏读,因为我们的data是普通对象,所以如果我们先释放写锁,别的线程恰巧获取到写锁,然后对数据进行了更该,而对于我们自己的线程是看不到被更改的内容;
class CachedData {
* Object data;
* volatile boolean cacheValid;
* final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
*
* void processCachedData() {
* rwl.readLock().lock();
* if (!cacheValid) {
* // Must release read lock before acquiring write lock
* rwl.readLock().unlock();
* rwl.writeLock().lock();
* try {
* // Recheck state because another thread might have
* // acquired write lock and changed state before we did.
* if (!cacheValid) {
* data = ...
* cacheValid = true;
* }
* // Downgrade by acquiring read lock before releasing write lock
* rwl.readLock().lock();
* } finally {
* rwl.writeLock().unlock(); // Unlock write, still hold read
* }
* }
*
* try {
* use(data);
* } finally {
* rwl.readLock().unlock();
* }
* }
* }}
读锁 与 tryReleaseShared
tryReleaseShared很简单,先减去当前线程持有的锁数量,再减去总体资源state中持有的数量;
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 (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// 释放读锁对读者没有影响,但是可能会允许等待着的写线程去处理当读和写都是自由时
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
写锁饥饿与悲观读
我们在非公平锁处看到过对写锁的特权,直接放行;也看到了对读锁的限制,当队首元素是写等待线程时,应该阻塞;Doug Lee 说这只是一种概率事件,还是可能会导致写锁饥饿;
所以,如果想避免写锁饥饿的话,可以使用公平锁来解决,新来的读线程,也应该入队,只要队列中其他等待线程。
可重入读写锁的读锁期间不允许写锁,所以这也是一种悲观读锁。