前言
前面几篇文章介绍了读锁的加锁和解锁流程,内容较多比较复杂,写锁的流程相对就简单一些了,根据源码来看一下具体流程。
加锁
// ReentrantReadWriteLock.WriteLock
public void lock() {
sync.acquire(1);
}
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
前面两步和ReentrantLock一样,通过lock方法进行加锁,然后调用AQS的acquire方法,先尝试去获取锁,如果成功了就结束,失败了就创建一个节点添加到阻塞队列进行等待,被唤醒之后继续尝试获取锁。
// ReentrantReadWriteLock.Sync
protected final boolean tryAcquire(int acquires) {
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;
}
首先获取锁的总数量和写锁的数量,如果锁的总数量不为0表示有读锁或者写锁
w == 0:写锁数量为0,说明此时有读锁,因为读写互斥,不能加写锁返回false;current != getExclusiveOwnerThread():有写锁而且持有锁的线程不是当前线程,因为写写互斥,所以加写锁失败返回false。- 上述两个条件都不满足的话,表示当前线程已经持有了写锁,现在继续尝试加写锁,也就是发生了写锁重入。如果写锁数量不会达到最大值,锁数量就加
1,因为写锁使用的是state的低16位,所以直接对state加1就可以了。
如果c == 0,表示此刻没有线程持有锁,但是阻塞队列是否有线程不确定,所以这里先调用了writerShouldBlock方法,在公平锁和非公平锁中分别有不同的实现。
// ReentrantReadWriteLock.NonfairSync
final boolean writerShouldBlock() {
return false;
}
在非公平锁的实现中直接返回了false,表示不管阻塞队列中是否有等待的线程,只要此时没有加锁当前线程就可以尝试去获取锁,所以是非公平的。
// ReentrantReadWriteLock.FairSync
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// AQS
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
而在公平锁的实现中,先去队列中查询是否有其他线程的节点正在等待,如果有的话当前线程就没有资格获取锁,必须遵循先来后到的准则到队列中去等待;如果没有其他等待的线程,那当前线程就可以去获取锁了,这就是公平的体现。
获取锁成功之后,通过setExclusiveOwnerThread方法把写锁的持有者修改为当前线程,然后返回true就结束了。
加锁失败
如果获取锁失败了,会继续执行第二个条件判断acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里分为两步:
addWaiter方法是把当前线程封装成Node节点添加到阻塞队列中;acquireQueued方法继续尝试去获取锁,如果失败了就通过park进行等待。
添加到阻塞队列
// AQS
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// AQS
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter方法首先把当前线程封装成Node节点,如果此时队列中有尾节点tail,就直接把当前节点添加到尾节点的后面,成为新的尾节点。
如果此时没有尾节点tail,说明队列还没有初始化,就调用enq方法先初始化队列,创建一个空节点(不关联线程)作为头结点head和尾节点tail,然后再把当前线程的节点添加到尾节点tail的后面,成为新的尾节点。
队列中尝试获取锁
// AQS
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// AQS
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// AQS
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
acquireQueued方法通过无限for循环来实现不断地获取锁-失败阻塞/成功退出。
- 首先获取判断前驱节点是否是头节点
head- 如果是的话表示当前线程有资格获取锁,就继续调用
tryAcquire方法去获取锁- 如果获取锁成功了,就把当前节点修改为头节点,以便能够唤醒后继节点。
- 如果获取锁失败了,向下执行阻塞流程。
- 不是的话表示当前线程没有资格获取锁,向下执行阻塞流程
- 如果是的话表示当前线程有资格获取锁,就继续调用
- 进入阻塞流程,判断是否应该阻塞
- 如果前驱节点的
waitStatus是Node.SIGNAL,表示具有唤醒后继节点的能力,当前节点就可以进行阻塞 - 如果前驱节点的
waitStatus是大于0,表示该节点被取消了,就继续向前找直到找到一个没被取消的节点,返回false继续尝试获取一次锁; - 如果前驱节点的
waitStatus是0或-3(waitStatus只有四个取值),就尝试把它修改为Node.SIGNAL(-1),返回false继续尝试获取一次锁; - 上面三种情况可以总结为,只要当前线程一直获取锁失败,就一定能得到一个具备唤醒能力的前驱节点,然后让当前线程去等待,最终一定能返回
true。
- 如果前驱节点的
- 如果可以阻塞,就调用
LockSupport.park(this)让当前线程进行等待,直到被其他线程唤醒或产生了中断。
释放锁
// ReentrantReadWriteLock.WriteLock
public void unlock() {
sync.release(1);
}
// AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放锁执行的是AQS的release方法,整体上分为两个步骤:
- 首先尝试释放锁,失败了就返回
false,成功了继续执行第二步; - 头节点如果存在并且具备唤醒能力,就去唤醒后继节点。
尝试去释放锁
// ReentrantReadWriteLock.Sync
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;
}
首先判断当前线程是否是持有写锁的线程,如果不是直接抛出异常。否则对锁数量减1,然后判断写锁数量是否变成0,如果是0,表示释放了所有的写锁,就把写锁持有者置为null;如果不是0,表示有锁重入,就只更新state的值。
唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
这个方法传入的参数是头节点head,首先尝试把head的waitStatus修改为0,是为了防止多个线程同时进入唤醒流程。
获取后继节点s,如果s是null获取标记为被取消,就从队列中倒序(个人理解为因为下一个节点是null或被取消,可能没法通过它来继续向后查找)查找符合条件并且离head节点最近的节点,如果存在这个节点,就调用LockSupport.unpark(s.thread)来把这个节点的线程唤醒,这里唤醒的线程是加锁失败后最终调用LockSupport.park()阻塞的线程。