一、前言
这篇文章主要是源码解读,帮忙理解读写锁里面的难点,也是作为自己对读写锁认识的一个总结。如果要掌握读写锁,看博客是远远不够的,必须回到jar包去品读源码,有不懂的地方再来看博客的注释部分。
二、概述
ReentrantReadWriteLock就是可重入的读写锁,其继承AQS类,内部实现原理自然就是改造过的CLH锁
- CLH锁是由Craig, Landin, and Hagersten这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。
- CLH锁是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
- CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
由CLH锁改良的读写锁可以说是自旋可重入锁,它是可以阻塞的,有公平和非公平两种实现。读写锁有如下特性:
- 写锁是独占锁,写锁被占用时,其他线程不能获得写锁和读锁,占用写锁的线程可以获得读锁(锁降级)。
- 读锁是共享锁,读锁被占用时,其他线程可以获得读锁。所有线程不能获得写锁。
- 加锁数量由32位整数表示。高16位是读锁数量,低16位是写锁数量,读写数各不超过65535个锁
三、读锁
在讲解加锁过程之前,我觉得有必要先来了解一下自旋锁节点的几个状态
CANCELLED = 1:取消状态。表示节点因为超时或者中断被取消。
SIGNAL= -1:信号量。 表示后继节点需要阻塞等待被唤醒(当锁释放或者取消时会唤醒后继节点)
CONDITION = -2:条件状态。表示节点在一个条件队列,在等待某个条件成立。此时该节点不会获取锁,除非条件满足。
PROPAGATE = -3:传播状态。 表示下一次竞争锁失败时可以再次参与竞争锁(这是看源码的自我理解)
0:默认状态。
好了,无须啰嗦,直接上加锁源码
1.读锁加锁过程
public void lock() {
// 获取共享锁
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// 获取读锁,如果小于0表示获取失败,需要执行doAcquireShared自旋获取锁
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
下面看看是如何获取锁的
// 尝试获取读锁,大于0说明获取成功
protected final int tryAcquireShared(int unused) {
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 {
// 这里把获得读锁的信息保存到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);
}
公平与非公平锁是通过readerShouldBlock方法两种实现来区分的。
非公平读锁
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// 如果头节点不为空,并且头节点的下一个节点不为空,并且头节点的下一个节点不是共享模式,
并且头节点的下一个节点线程不为空,则返回true,表示需要放弃锁竞争,让给后面的写锁去竞争锁。
这里其实就是读锁避让一下写锁,让写锁不至于饥饿。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
公平锁读锁
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 如果头节点不等于尾节点(说明是有等待锁的节点),并且头节点的下一个节点为空或者
头节点的下一个节点线程不等于当前线程,则返回true,放弃读锁竞争。
这时说明队列中有节点在等待锁,所以为了公平,先进先出,当前线程必须加到队列尾部等待
获取读锁。这里判断h.next == null 是因为当第一个节点插入队列时,
头节点可能会为空,如下addWaiter方法可以看出
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 如果是第一个节点入队列,这里tail=head,pred=head
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 这里把新节点设置为tail了,此时head != tail
if (compareAndSetTail(pred, node)) {
// 下面这行代码执行之前,head.next == null
pred.next = node;
return node;
}
}
enq(node);
return node;
}
当第一次获取锁失败时,不是立刻返回失败,而是再次进入轮询获取锁。fullTryAcquireShared主要作用是防止死锁的发生。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (; ; ) {
int c = getState();
// 判断是否有写锁获取了独占锁
if (exclusiveCount(c) != 0) {
// 这里说明其他线程获取了独占锁,可以立刻返回获取失败
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 进到这里就是为了避免重入锁阻塞,如果重入锁阻塞了,
// 那么锁就不放释放,后面的节点就会发生锁饥饿。
// 这里用readerShouldBlock()方法判断是因为之前的tryAcquireShared方法
// 直接遇到readerShouldBlock方法为true时是直接进来fullTryAcquireShared方法的。
if (firstReader == current) {
// 如果第一个获取读锁的是当前线程,那么直接跑下面代码去竞争锁
// assert firstReaderHoldCount > 0;
} else {
// 这个分支就是判断是否为重入的读锁,如果当前线程获取的读锁数为0,
// 那么获取锁失败,否则是重入锁,需要参与锁竞争。
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
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;
}
}
}
下面讲解第一阶段获取锁失败时,节点会被加入队列,自旋获取锁
// 这个方法主要是自旋获取锁,如果获取不了则进入阻塞队列等待前面节点唤醒
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 如果前一个节点是头节点,则可以参与锁竞争获取锁。
// tryAcquireShared方法上面有讲解过
int r = tryAcquireShared(arg);
if (r >= 0) {
// 设置当前节点为头节点,并且唤醒后面的连续的读锁,
// 即唤醒后面的读锁参与锁竞争。
// 如果下一个读锁又获得锁还会唤醒后面读锁参与竞争
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 判断是否立刻阻塞,有可能还不需要阻塞,可以继续参与锁竞争。
// 后面解读shouldParkAfterFailedAcquire时会讲到
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
下面方法是唤醒后面的读锁
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 这里两次判断了h是否为空,两次都是为了检验头节点是否还是为空(有可能第一次是空,
// 第二次不为空了,因为这里是不加锁的判断,头节点随时会变化。
// 如果第二次不为空就判断他的等待状态是否小于0.之所以h为null还可以唤醒后面节点,
// 是因为判断为null的时候,可能有节点正在入队列,为了不错过唤醒读锁,
// 所以会让它进入doReleaseShared方法去唤醒后面的读锁)
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 后面讲解这方法
doReleaseShared();
}
}
唤醒读锁的方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果头节点等待状态为signal(表示后面节点需要阻塞),
// 则把头节点等待状态改为0,表示后面节点无须等待
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继节点,后面讲解
unparkSuccessor(h);
}
// 如果头节点已经是0,则直接改为PROPAGATE状态,
// 则后面节点竞争锁失败时不会马上阻塞,会再次竞争锁
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头节点没有改变说明唤醒完毕,退出轮询。
// 如果头节点改变了,说明其他线程获取到了锁,为了让唤醒传递下去,继续轮询
// 这里思考好久,会不会一直不相等呢,其实如果要一直不相等,就得后面唤醒的线程
// 获得锁并更改了head,但是后面线程涉及到线程调度,不可能一直比这里执行得快
// 所以看代码是可能会一直不相等,但是实际运行起来是不可能的
if (h == head) // loop if head changed
break;
}
}
唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 等待状态设置为0,表示后继节点参与锁竞争时无须入队列等待锁
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果后继节点为空,或者被取消,
// 则需要从尾节点轮询找到下一个可以被唤醒的有效节点
// (为什么要从后面轮询,是因为入队列时,h.next有可能为null)
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);
}
当竞争锁失败时判读是否阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果前驱节点是signal,则表示需要阻塞当前线程
return true;
if (ws > 0) {
// 如果前驱节点被取消了,则向前找到未被取消的节点,
// 把当前节点加到后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果等待状态是0,或者是传播(PROPAGATE)状态,
// 则把前驱节点设置为sigal。当前节点可以再次参与锁竞争一次,
// 如果还是失败,那么就会直接进入队列等待锁。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.读锁解锁过程
如果理解了以上加锁过程,那么解锁就很容易理解了,直接看代码吧。
// 这里不解释sync了,其实它就是AQS的子类
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放共享锁,如果返回true,说明需要唤醒后继节点,否则直接返回
if (tryReleaseShared(arg)) {
// 唤醒后继节点,这个方法在上面解锁过程解析过,不再讲解
doReleaseShared();
return true;
}
return false;
}
// 尝试解锁过程
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))
// 只有在没有读锁和写锁的情况下才需要唤醒后继节点,
// 因为如果队列有读锁的话,读锁加锁过程会唤醒后面读锁,
// 如果队列有写锁,那么读写、写写都是是互斥的,不需要唤醒。
return nextc == 0;
}
}
四、写锁
1. 写锁加锁过程
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
// acquireQueued方法跟doAcquireShared类似就不讲解了,直接看源码就好
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 尝试获取写锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// c不等于0表示有锁
if (c != 0) {
// w等于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;
}
// writerShouldBlock方法的公平锁实现是判断队列是否有节点,
// 非公平锁直接是false,不需要避让
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
2. 写锁解锁过程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 尝试解锁
if (tryRelease(arg)) {
Node h = head;
// 唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 判断独占锁的数量,因为写锁也是可以重入的,所以不一定写锁加锁数是0
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
// 因为是独占锁,所以这里可以不加锁直接改变加锁的数量
setState(nextc);
return free;
}
五、总结
读写锁花不少时间去看源码,里面有些逻辑不是看看就可以理解的,必须要多问问自己为什么是这样写。只能惊叹作者为了不让机器在同步锁上消耗性能,直接折腾自己的脑袋了。其实我也是佩服AQS算法的精妙,既能保护共享资源,又能保证效率。
文章解析部分是基于自己的理解,有不正确的地方欢迎指正。