目录
- 1. 简介
- 2. 从ReentrantLock的角度看AbstractQueuedSynchronizer
- 3. 从Semaphore的角度看AbstractQueuedSynchronizer
- 4. CountDownLatch源码分析
- 5. CyclicBarrier源码分析
- 6 ReentrantReadWriteLock源码分析
- 7 写在最后
1. 简介
AbstractQueuedSynchronizer,又叫队列同步器,是java.util.concurrent包下的核心抽象类。我们所熟知的ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReentrantReadWriteLock等并发工具类都是基于AbstractQueuedSynchronizer实现的。
ReentrantLock等并发工具类是锁的实现,是直接提供给Java程序员使用的,而AbstractQueuedSynchronizer是实现锁的基础。一般我们将AbstractQueuedSynchronizer的某个子类A作为静态内部类,然后利用该子类A去具体实现我们的锁。给出JDK中提供的一个不可重入互斥锁的例子:
public class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() {
return new ConditionObject();
}
// Deserializes properly
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
在上述Mutex的实现中我们可以看到,Sync是Mutex中的一个静态内部类,继承了AbstractQueuedSynchronizer。Mutex中对外暴露的方法,几乎都是通过持有的Sync实例sync来完成的。而Sync中的方法,则是通过调用其父类AbstractQueuedSynchronizer中的方法来完成的。Mutex的实现很简单,但也是我们使用AbstractQueuedSynchronizer最常见的一种方式,即子类根据自己的需要,实现AbstractQueuedSynchronizer中的这5个方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
我们可以看到,上述5个方法在AbstractQueuedSynchronizer中默认都会抛出UnsupportedOperationException异常。子类可以根据需要有选择性地去实现其中的方法,比如独占锁中我们一般会实现tryAcquire()、tryRelease()和isHeldExclusively()方法,而共享锁中我们一般会实现tryAcquireShared()和tryReleaseShared()方法,如果是读写锁,我们可能会将这5个方法都实现。
先来说一下AbstractQueuedSynchronizer的设计思想:
- 持有一个volatile修饰的int型成员变量state,该state代表一种同步状态,具体代表什么由子类自行定义。并提供了getState()、setState()和compareAndSetState()三个方法用来读取或改变state的值。
- 定义了一个先进先出的同步队列,队列中的节点由AbstractQueuedSynchronizer中的一个静态内部类Node定义。无论是独占锁还是共享锁,都可以通过继承AbstractQueuedSynchronizer来实现,独占锁和共享锁的线程由Node类抽象。
- 借助LockSupport.park()方法和LockSupport.unpark()方法来使某线程等待和唤醒某线程。
1.1 同步队列和条件队列
AbstractQueuedSynchronizer用两个属性head和tail来描述同步队列,head代表同步队列的头节点,tail代表同步队列的尾节点。关于同步队列,有2点注意事项:
-
同步队列是延迟初始化的,如果所有的线程都能获取到锁,没有出现阻塞的情况,那么同步队列是不会被初始化的,即head和tail节点都为null。只有当有线程发生阻塞等待时,同步队列才会被初始化。
-
同步队列的head节点是不持有任何线程的,是一个虚节点,非首节点才持有阻塞在同步队列中的线程。
条件队列是和同步队列不一样的概念。条件队列是绑定在ConditionObject上的,ConditionObject是AbstractQueuedSynchronizer中的一个内部类,实现了Condition接口。一个独占锁可以和多个Condition绑定,而一个Condition持有一个条件队列,即一个独占锁可以有多个条件队列。我们现在只需要知道有这么一个概念即可,后续会详细说明。
1.2 线程的抽象——Node节点的定义
不管是共享锁还是独占锁,也不管是在同步队列中等待还是在条件队列中等待,等待获取锁的线程都会被封装成为一个Node节点,来看一下其中的关键属性:
// waitStatus代表当前Node节点的状态
volatile int waitStatus;
// 同步队列是一个双向链表,由prev和next两个属性串联起来
volatile Node prev;
volatile Node next;
// 当前Node节点代表的线程
volatile Thread thread;
// 条件队列是一个单向链表,由nextWaiter属性串联起来
Node nextWaiter;
waitStatus有5种定义:
- CANCELLED,值为1,表示该节点因为超时或者中断,已经被取消,被取消的节点不再争锁。这是一种终止状态,一个节点一旦进入CANCELLED状态就不可能转变为其它状态。
- SIGNAL,值为-1,表示该节点有义务去唤醒后继节点持有的线程。
- CONDITION,值为-2,表示该节点是条件队列中的节点。
- PROPAGATE,值为-3,用于共享锁节点。对于独占锁节点,我们只会在持有锁的线程释放锁时去唤醒后继节点。而对于共享锁节点,某个线程获取锁成功时,该线程也有义务去唤醒后继节点(为什么呢?因为这是共享锁节点,既然该线程获取锁成功了,后续线程也可以被唤醒去争锁),PROPAGATE描述的就是这样一种状态。
- 0,这是节点的初始状态,不代表任何含义,一般head节点是该状态。
只讲AbstractQueuedSynchronizer太过抽象,本文会结合ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReentrantReadWriteLock这5个工具类来逐行代码地讲解AbstractQueuedSynchronizer的实现。
2. 从ReentrantLock的角度看AbstractQueuedSynchronizer
ReentrantLock是一个可重入独占锁,和synchronized关键字很像,但是ReentrantLock比synchronized能够实现的功能更丰富:
- ReentrantLock可以实现公平锁,synchronized关键字无法实现公平锁。
- ReentrantLock可以无阻塞地尝试获取锁,synchronized关键字只能用阻塞的形式等待获取锁。
- ReentrantLock可以响应中断,synchronized关键字无法响应中断。
- ReentrantLock可以设置获取锁的最大超时时间,synchronized关键字无法设置获取锁的最大超时时间,只会一直阻塞等待获取锁。
- ReentrantLock可以绑定多个条件队列,这些条件队列都绑定在ConditionObject实例对象上,synchronized关键字只能有一个条件队列,该条件队列绑定在持有锁的对象上。
当然,相比synchronized关键字,ReentrantLock的使用更加复杂。对于synchronized关键字,由JVM来保证每次进入同步代码块的线程只有一个,出了同步代码块后锁会自动释放,由字节码指令monitor enter和monitor exit来实现。而对于ReentrantLock,锁的获取和释放都需要由Java程序员手动编码实现,一个线程重入几次锁,就需要在finally语句块中释放几次锁。我们可以看一下JDK给我们提供的使用示例:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
在ReentrantLock中,AbstractQueuedSynchronizer中的成员变量state代表的是锁的重入次数:state为0代表没有任何线程持有锁,state=x代表有x个线程持有锁。每次调用lock()方法成功时,会将state值加1,调用unlock()方法成功时,会将state值减1,只有当state减为0时才代表释放锁。当然,除了上述阻塞式地获取锁lock()方法之外,ReentrantLock还提供了非阻塞式地获取锁tryLock()方法、可以设置获取锁的最大超时时间的tryLock(long, TimeUnit)方法。
2.1 公平锁和非公平锁
公平锁和非公平锁,是在初始化ReentrantLock实例对象的时候根据传入的布尔参数决定的,传入true代表公平锁,传入false或者不传任何参数代表非公平锁。相比于公平锁,非公平锁能维持更大的线程吞吐量,性能更好,但可能导致线程饥饿。公平锁和非公平锁相关的类图如下:
Sync继承自AbstractQueuedSynchronizer,是ReentrantLock中的一个静态内部类。NonFairSync和FairSync是Sync的两个子类,分别代表非公平锁和公平锁的实现,我们可以来看看这两个类中方法的区别。
// 非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平锁
final void lock() {
acquire(1);
}
在lock()方法中,我们可以看到,对于非公平锁,一开始就会通过compareAndSetState()尝试修改state的状态,如果修改成功,说明获取到了锁,设置当前持有锁的线程是当前线程。如果修改失败,则会乖乖地调用AbstractQueuedSynchronizer的acquire(1)方法去同步队列里排队等待。而对于公平锁,为了线程严格的先来后到,会直接调用acquire(1)方法去同步队列里乖乖排队等待。
总结一下区别:非公平锁相对于公平锁,lock()方法多了一次在一开始的时候的争锁尝试,其余都是相同的。
// 非公平锁(为方便比较,这里对源码做了修改。对于非公平锁NonFairSync,tryAcquire()方法调用的是Sync中的nonfairTryAcquire()方法。)
protected final boolean tryAcquire(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;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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");
setState(nextc);
return true;
}
return false;
}
在上述tryAcquire()方法的比较中,我们发现非公平锁和公平锁的唯一区别就是:在发现当前state为0,即没有线程持有锁时,非公平锁会直接CAS尝试修改state的状态来获取锁,而公平锁则还需要调用hasQueuedPredecessors()去判断当前同步队列里是否有线程在等待着获取锁,如果有线程在等待着获取锁,为了保证公平性,当前线程是不能去争锁的,如果没有线程在等待着获取锁,那么当前线程可以尝试去获取锁。
我们来看一下hasQueuedPredecessors()方法的实现:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head == tail,说明同步队列未被初始化,此时没有任何节点在等待,直接返回false。
// 如果head.next == null,注意此时已经有head != tail了,说明除了当前线程外,有新节点正在入队,
// 这一点可以在addWaiter()方法中得到解释,此时返回true。
// 如果head != tail且head.next != null,我们看一下head.next持有的线程是否是当前线程,如果是,说明没有任何先来
// 的节点在等待,返回false。否则,返回true,head.next才是下一个应该争锁的节点,当前线程不是。
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
总结一下区别:公平锁相对于非公平锁,tryAcquire()方法在争锁前多了一个检查,检查同步队列中是否有线程正在等待获取锁。也正是因为这个判断,公平锁比非公平锁的性能要低,吞吐量要低,但能防止线程饥饿。
2.2 acquire()方法
在公平锁和非公平锁的lock()方法中,我们看到了acquire(1)方法的调用,该方法是定义在AbstractQueuedSynchronizer中的:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.2.1 tryAcquire()方法
tryAcquire()方法是在子类NonfairSync和FairSync中实现的,在2.1 公平锁和非公平锁中我们已经提及过,这里就简单描述一下这个方法的作用:无阻塞地尝试获取锁,如果获取锁成功,返回true,如果获取锁失败,返回false。
如果tryAcquire()方法返回true,代表本次获取锁成功,既然成功了就不需要再进入同步队列去排队等待了,这个if语句的判断结果为false,不会执行&&后面的判断条件了。
如果tryAcquire()方法返回false,那么我们会执行&&后面的判断条件。
2.2.2 addWaiter()方法
addWaiter()方法是AbstractQueuedSynchronizer中实现的,其作用就是将当前线程包装成Node节点,排队等候在同步队列的末尾,我们可以来看一下这个方法的实现:
private Node addWaiter(Node mode) {
// 将当前线程Thread.currentThread()包装成一个新的Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 获取队列的尾节点
Node pred = tail;
if (pred != null) {
// 这里可能出现多线程竞争入队的情况,需要用CAS操作。
// 如果尾节点不为空,就尝试将当前node节点添加为尾节点的后继节点,这里的操作十分精巧:
// 1. 先将node节点的prev指针设置指向原tail节点。
// 2. 再CAS重新设置tail节点的值。
// 3. 最后才设置尾节点的next指针。
// 这样做就始终保证了从尾节点通过prev指针往前遍历,能够遍历到同步队列中的所有节点,不会出现有节点被漏掉的情况。
// 如果先CAS设置tail节点的值,再设置node节点的prev指针,这两个操作并不能保证原子性。
// 在多线程环境下,可能出现某个瞬间tail节点的prev指针还没有被设置的情况,那么从尾节点通过prev指针往前遍历,可能有节点会被遗漏。
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
// 设置成功了,将node节点返回
return node;
}
}
// 走到这里,说明tail节点为null,或者当前线程CAS竞争设置尾节点失败了,那么会调用enq()方法将node节点添加进同步队列的末尾
enq(node);
return node;
}
// enq() 方法是一个死循环,跳出循环的只有一种情况,那就是节点入队成功。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果尾节点为null,说明此时同步队列还没有初始化,初始化同步队列,其head和tail节点均指向一个新的Node节点。
// 初始化完之后,还会进入下一轮for循环,这个时候尾节点就不为null了,会进入else分支。
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 这里的操作和addWaiter()中的是一样的,只不过这里在一个死循环里,只有入队成功才会跳出该循环。
// 我们可以这么理解,addWaiter()中只是尝试入队一次,不成功也没关系,有enq()方法强制入队来兜底。
// 而enq()方法通过死循环+CAS操作的方式强行入队,这是Java并发编程中一种常见的技巧。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.2.3 acquireQueued()方法
2.2.3.1 中断的作用
节点阻塞和争锁都在acquireQueued()方法里,其返回值是一个布尔类型,代表在节点持有的线程在阻塞直到获取锁成功时是否发生过中断。事实上,无论是否发生过中断,对该方法的执行不会产生任何影响,中断只会影响该方法的返回值。我们再来看一下该方法被调用的地方:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们看到如果acquireQueued()方法的返回值标记有中断产生,那么会调用selfInterrupt()方法重置中断标记位:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
总结一下,中断对acquire()方法的执行没有任何影响,仅仅设置了线程的中断标志位。如果想要通过中断来取消当前线程,可以调用acquireInterruptibly()方法,而不是acquire()方法。
2.2.3.2 总流程
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)) {
// 只有当前节点的前驱节点是head,当前节点才会去争锁。
// 只有当前节点争锁成功时,才会进入到这里。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 死循环的唯一出口(当然,如果发生异常也能跳出死循环)
}
// shouldParkAfterFailedAcquire()判断在争锁失败后(可能是因为当前节点的前驱节点并不是head节点,
// 也可能是因为有其他线程在同时调用tryAcquire()方法抢锁),当前线程是否可以挂起。
// 如果可以挂起,就会调用parkAndCheckInterrupt()方法挂起当前线程,并检查中断标记是否为true,如果为
// true,则会将interrupted置为true。
// 如果不能挂起,继续进入下一轮循环尝试去争锁。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 只有通过异常跳出上述死循环,failed才会是true,此时当前节点需要被取消
cancelAcquire(node);
}
}
总结一下上述流程如下:
在一个死循环里,如果node节点的前驱节点是head节点,node节点会尝试去获取锁,获取锁成功是跳出死循环的唯一出口。同时,在每一轮循环里,在获取锁失败后会判断当前线程是否能够挂起,如果能够挂起,就会挂起当前线程并检查是否有中断发生。如果不能够挂起当前线程,会进入下一轮循环。除了正常获取锁成功退出循环外,如果发生了异常,也是会跳出该死循环的,这时候获取锁是失败的,我们需要将该node节点设置为CANCELLED节点,代表该节点因为异常而放弃争锁。
2.2.3.3 shouldParkAfterFailedAcquire()方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果前驱节点是SIGNAL节点,node节点是可以挂起的,由前驱节点来负责唤醒node节点
return true;
if (ws > 0) {
// 只有CANCELLED节点的waitStatus是大于0的
// 如果前驱节点是CANCELLED节点,我们需要寻找到一个非CANCELLED的节点作为node节点的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驱节点不是CANCELLED节点,CAS尝试将前驱节点的waitStatus设置为SIGNAL
// 这个操作成功或者失败都是没有关系的。
// 如果这个操作成功了,下一轮循环就会进入if(ws == Node.SIGNAL)这个分支。
// 如果这个操作失败了,下一轮循环还会进入到这里。
// 本质上这里也是死循环+CAS的操作,只是这个死循环在方法调用的外层。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
总结一下shouldParkAfterFailedAcquire()的逻辑:
寻找一个非CANCELLED的前驱节点,将其状态设置为SIGNAL,这样就能够挂起node节点了,node节点的唤醒由其前驱节点来完成。否则,node节点不能挂起,因为如果挂起,没有前驱节点来负责唤醒node节点。
2.2.3.4 parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt()方法的逻辑很简单:
挂起当前线程,返回值代表线程是否发生了中断。需要注意的是,Thread.interrupted()方法会清除中断标志位,我们可以看到在parkAndCheckInterrupt()的调用处,用一个interrupted的变量来记录该中断,作为acquireQueued()方法的返回值。再在这里贴一下acquire()方法的逻辑:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们可以看到,如果acquireQueued()返回true,说明发生了中断,会调用selfInterrupt()重新设置中断标志位。
2.2.3.5 cancelAcquire()方法
private void cancelAcquire(Node node) {
// 由于是acquireQueued()方法死循环中发生异常才调用的这个方法,所以对node做一个判空操作。
if (node == null)
return;
node.thread = null;
// node节点要被移出同步队列了,为node节点的后继节点寻找一个非CANCELLED状态的前驱节点。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
// 如果node节点是尾节点,且CAS重置尾节点成功,将前驱节点的next置为null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果pred是head节点,node节点被取消了,那么我们可以直接唤醒后继节点去尝试争锁。
// 如果pred持有的线程是null,说明不会有线程来唤醒后继节点了,直接唤醒node节点的后继节点去尝试争锁。
// 如果pred不是head节点,但是pred是一个SIGNAL节点,说明pred节点有唤醒后继节点的责任,此时如果node节点的后继节点
// 存在且其状态不是CANCELLED节点,则将pred节点的后继节点设置为node节点的后继节点,这样就把node节点清除了出去。如
// 果node节点的后继节点为null,或者node节点的后继节点也是一个CANCELLED节点,是不需要将pred的节点后继设置为node
// 节点的后继节点的,因为node节点和node节点的后继节点都是CANCELLED节点,是否把node节点清除出去对同步队列没有任何
// 影响。这些CANCELLED节点的出队操作在shouldParkAfterFailedAcquire()方法中进行。
// 如果前驱节点不是CANCELLED且CAS设置其状态为SIGNAL成功,可以将pred节点的后继节点设置为node节点的后继节点,其唤
// 醒由pred节点负责。
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
2.2.3.5.1 unparkSuccessor()方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// CAS设置node节点的waitStatus为0,表明其已经进行了唤醒后继节点的操作,不再有唤醒后继节点的责任。
// 这里设置失败也没有关系,因为说明有其他线程设置成功了。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 如果node节点的后继节点为null,或者node节点的后继节点是CANCELLED节点,我们需要从后往前沿着prev指针遍历去
// 寻找第一个需要唤醒的节点。
// 为什么node节点的后继节点为null了,但是还需要沿着prev指针从后往前遍历呢?
// 在addWaiter()和enq()方法中,我们将新节点入队时,先设置了新节点的prev指针,再CAS入队,最后再设置原尾节点
// 的next指针。多线程环境下,很可能存在一个时刻,node节点的next指针为null,但是node节点有后继节点,如果沿着
// prev指针,从tail节点往前找,一定能够找到需要唤醒的该节点。
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒同步队列中的第一个待唤醒的节点,该节点排在node节点之后(并不一定是node节点的直接后继节点)
LockSupport.unpark(s.thread);
}
2.3 acquireInterruptibly()方法
acquire()方法是不响应中断的,acquireInterruptibly()方法是响应中断版本,来看看具体实现:
public final void acquireInterruptibly(int arg) throws InterruptedException {
// 在方法一开始处就对中断标记做一个判断,如果发生中断了直接抛出InterruptedException异常,这是响应中断的方法的编程套路
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
// 在争锁失败后,会调用doAcquireInterruptibly()方法,该方法会将该节点加入同步队列,并阻塞或死循环争锁
doAcquireInterruptibly(arg);
}
2.3.1 doAcquireInterruptibly()方法
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 对比acquireQueued()方法的唯一不同之处
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireInterruptibly()比acquireQueued()方法多了一步:将线程包装成node节点加入同步队列里。acquireQueued()是将node节点作为参数传进来的。另外,acquireQueued()的返回值代表着是否发生了中断,doAcquireInterruptibly()没有返回值,因为如果发生了中断会直接抛出InterruptedException异常。其余逻辑和acquireQueued()一模一样,不再分析。
2.4 tryAcquireNanos()方法
tryAcquireNanos()方法是带有超时时间的tryAcquire()版本,同时该方法会响应中断,来看看具体的实现逻辑:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 在方法一开始处就对中断标记做一个判断,和acquireInterruptibly()一样的编程套路
if (Thread.interrupted())
throw new InterruptedException();
// 调用tryAcquire()方法争锁成功,那么直接返回true,表示获取锁成功。
// 否则,需要调用doAcquireNanos()方法去争锁,该方法响应中断且对超时时间有限制。
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
2.4.1 doAcquireNanos()方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// nanosTimeout记录的是当前距离deadline还剩下多少时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
// 如果已经超时,说明争锁失败
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
// spinForTimeoutThreshold是自旋阈值,默认是1000,如果距离deadline剩余的时间小于等于
// spinForTimeoutThreshold,当前线程不会挂起,只会进入下一个循环继续去争锁,相当于线程
// 在自旋争锁。如果距离deadline剩余的时间大于spinForTimeoutThreshold,当前线程会挂起,
// 但是这个挂起是有时间限制的,如果挂起时长到达了nanosTimeout,该线程会恢复运行,这个时候
// 进入下一轮循环就会从if (nanosTimeout <= 0L)分支返回,代表超时获取锁失败。
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们可以看到doAcquireNanos()方法和doAcquireInterruptibly()方法、acquireQueued()方法很相似,只是多了对超时逻辑的处理。
2.5 条件队列
2.5.1 条件队列的概念
以synchronized关键字为例来介绍条件队列的概念,synchronized锁绑定在一个对象上,即某对象持有synchronized锁。条件队列有且仅有一个,且绑定在持有synchronized锁的对象上。这也是为什么和条件队列相关的方法wait()和notify()都定义在Object类中的原因。来看一个例子:
public class ConditionTest {
private static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
System.out.println(Thread.currentThread().getName() + "阻塞等待");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
}
}, "线程1");
Thread thread2 = new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
System.out.println(Thread.currentThread().getName() + "唤醒正在等待的线程");
object.notify();
System.out.println(Thread.currentThread().getName() + "唤醒成功");
}
}, "线程2");
thread1.start();
Thread.sleep(1000);
thread2.start();
}
}
线程1先获取到锁,调用object.wait()方法使其等待在条件队列中。线程2获取到锁后,调用object.notify()方法将线程1从条件队列中唤醒。上述程序的输出结果如下:
线程1获取到了锁
线程1阻塞等待
线程2获取到了锁
线程2唤醒正在等待的线程
线程2唤醒成功
线程1被唤醒
关于synchronized关键字的条件队列有3个注意点:
- 该条件队列不保证先进先出,即object.notify()方法唤醒条件队列中等待的线程是随机唤醒。如果想唤醒条件队列中所有等待的线程,可以调用notifyAll()方法。
- 必须持有锁,才能调用object.notify()、object.wait()和object.notifyAll()方法。
- synchronized关键字只能绑定一个条件队列。
相比于synchronized关键字的条件队列,ReentrantLock的条件队列能够保证先进先出,具有公平性,且一个ReentrantLock可以有多个条件队列,对应各种不同的条件,适用面更广。最简单的一个应用就是,我们可以很容易地利用ReentrantLock实现一个阻塞队列:
public class MyBlockingQueue {
private ReentrantLock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
private Object[] items = new Object[10];
private int takeIndex;
private int putIndex;
private int size;
public void put(Object object) throws InterruptedException {
lock.lock();
try {
if (size == items.length) {
notFull.await();
}
items[putIndex++] = object;
if (putIndex == items.length) {
putIndex = 0;
}
size++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
if (size == 0) {
notEmpty.await();
}
Object result = items[takeIndex++];
if (takeIndex == items.length) {
takeIndex = 0;
}
size--;
notFull.signal();
return result;
} finally {
lock.unlock();
}
}
}
我们的阻塞队列可以至多存放10个元素,使用ReentrantLock我们可以轻松构建出notEmpty和notFull两个条件队列,来分别对应我们的阻塞队列不为空和阻塞队列不满的情况。
2.5.2 AbstractQueuedSynchronizer中条件队列的实现
简单概括下AbstractQueuedSynchronizer中的条件队列的实现思想:
- 调用await()方法会将某个节点从同步队列移动至条件队列等待唤醒,调用await()方法的前提是当前线程持有锁。
- 调用signal()方法会唤醒条件队列中等待的第一个节点,将其加入同步队列中,调用signal()方法的前提也是当前线程持有锁。
条件队列在AbstractQueuedSynchronizer由其内部类ConditionObject来抽象,ConditionObject主要含有2个属性:
// 条件队列的首节点
private transient Node firstWaiter;
// 条件队列的尾节点
private transient Node lastWaiter;
2.5.2.1 await()方法
public final void await() throws InterruptedException {
// await()方法是响应中断的,参考tryAcquireNanos()和acquireInterruptibly(),
// 在方法一开始对中断标志位做一个判断,是一种编程范式。
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程包装成Node节点加入条件队列中
Node node = addConditionWaiter();
// 释放锁,savedState是当前线程的重入次数
int savedState = fullyRelease(node);
int interruptMode = 0;
// 跳出下述while循环的情况有2种:
// 1. node节点已经转移至同步队列中。
// 2. interruptMode不为0,即发生了中断。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// node节点尝试去获取锁,如果获取锁成功,且interruptMode不为THROW_IE,则将
// interruptedMode记为REINTERRUPT。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果node.nextWaiter不为null,说明线程并不是被signal唤醒的,而是被中断唤醒的。
// 此时,当前节点需要被取消。
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
// 如果发生过中断,根据interruptMode来报告中断情况
reportInterruptAfterWait(interruptMode);
}
2.5.2.1.1 addConditionWaiter()方法
addConditionWaiter()方法用于将当前线程加入条件队列中,注意,此时不需要考虑线程安全问题,因为await()方法的调用前提是获取到锁。来看一下这个方法的实现:
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
// 如果条件队列的尾节点已经被取消了,将条件队列中所有被取消的节点都清除出去
// unlinkCancelledWaiters()方法就是单纯的链表操作,把不是CONDITION的节点都清除出去,就不再贴代码了
unlinkCancelledWaiters();
t = lastWaiter;
}
// 将当前节点插入到条件队列的尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
2.5.2.1.2 fullyRelease()方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
// 如果释放锁失败,说明当前线程在没有持有锁的情况下调用了await()方法,直接抛出
// IllegalMonitorStateException异常
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
// 如果try方法里抛出了IllegalMonitorStateException异常,就会走到这个逻辑,将当前节点取消
// 该节点会在其他线程调用unlinkCancelledWaiters()方法时被清除出去
node.waitStatus = Node.CANCELLED;
}
}
fullyRelease()方法的作用是释放锁,将state重置为0。我们再来看一下其中release()方法的实现:
public final boolean release(int arg) {
// tryRelease()方法在ReentrantLock.Sync类中定义,尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 如果head的waitStatus不为0,说明其有义务唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()方法定义在ReentrantLock的静态内部类Sync中,其实现如下:
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;
}
可以看到,tryRelease()方法的逻辑很简单,调用tryRelease()方法的前提是当前线程持有锁,否则会抛出IllegalMonitorStateException异常。如果tryRelease()方法将state状态置为0,那么就需要将持有独占锁的线程设置为null,并在release()方法中唤醒后继节点,表示当前线程已经完全释放了锁。
2.5.2.1.3 interruptMode的含义
interruptMode有3种取值:
- 0,表示没有发生任何中断。
- THROW_IE,值为-1,表示中断信号发生在signal()信号之前,在退出await()方法时需要重新抛出InterruptedException异常。
- REINTERRUPT,值为1,表示中断信号发生在signal()信号之后,在退出await()方法时只需要重新设置中断标志位,无需抛出InterruptedException异常。
2.5.2.1.4 isOnSyncQueue()方法
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
// 如果node节点是CONDITION状态,显然不在同步队列上,在同步队列上的节点状态只有3种:0、SIGNAL和PROPAGATE。
// 如果node节点的prev指针为null,显然不在同步队列上,在addWaiter()方法中我们已经分析过,同步队列中的节点,
// 除了首节点外,prev指针不可能为null。而首节点是不持有等待线程的。
return false;
if (node.next != null) // If has successor, it must be on queue
// 在addWaiter()方法中我们已经分析过,如果next指针不为null,说明节点肯定已经在同步队列中
return true;
// 从同步队列队尾开始寻找node节点,为什么从队尾开始找?在addWaiter()方法我们已经分析过这个问题
return findNodeFromTail(node);
}
2.5.2.1.5 checkInterruptWhileWaiting()方法
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
checkInterruptWhileWaiting()方法的逻辑很简单:
-
如果没有发生过中断,直接返回0。
-
如果发生过中断,判断中断发生在signal()信号之前还是之后。如果中断发生在signal()信号之前,返回THROW_IE;如果中断发生在signal()信号之后,返回REINTERRUPT。
transferAfterCancelledWait()方法就是用于判断中断发生在signal()信号之前还是之后的:
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// CAS操作成功地将node节点的状态置为了0,说明是中断将线程唤醒的,为什么呢?因为在signal()方法中会将CONDITION置为
// 0。如果signal()方法先于中断,此处应该CAS失败。中断唤醒的节点也是会加入同步队列的,会去争锁。但是其nextWaiter属
// 性不为null。联系await()中的unlinkCancelledWaiters()方法,条件队列中的非CONDITION节点会被清除出去,该节点
// 也会在这个方法被清除出去。
enq(node);
return true;
}
while (!isOnSyncQueue(node))
// 走到这里,说明唤醒线程的是signal()信号,不是中断,此时该线程已经被移除出了条件队列,但是还没有进入同步队列。
// 这里我们选择用Thread.yield()方法主动让出CPU时间片,等待另外的线程将node节点转移至同步队列中。
Thread.yield();
return false;
}
2.5.2.1.6 reportInterruptAfterWait()方法
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
reportInterruptAfterWait()方法的逻辑很简单,就是根据interruptMode来选择重置中断标志位或者抛出InterruptedException异常。
2.5.2.2 signal()方法
signal()方法至多只会将条件队列中的一个节点转移至同步队列中,转移时遵循先来后到原则,具有公平性。
public final void signal() {
if (!isHeldExclusively())
// 只有持有锁才能调用signal()方法,否则会抛出IllegalMonitorStateException异常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
真正执行唤醒的逻辑是在doSignal()方法中的:
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
doSignal()方法的逻辑也比较简单,从条件队列队首开始沿着nextWaiter指针遍历,寻找第一个节点将其转移至同步队列中,如果该节点转移失败,则选取其后继节点转移。总之,最多只可能将条件队列中的一个节点转移至同步队列中。转移至同步队列中的节点,其nextWaiter属性为null。在前面await()方法的分析中,我们可以看到通过判断node的nextWaiter属性是否为null,来判断该节点是被中断唤醒的,而不是被signal()信号唤醒的,此时也会调用unlinkCancelledWaiters()方法将该节点清除出去。
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 如果CAS操作失败,说明该节点已经被取消,返回false,不将其加入同步队列
return false;
// 将该节点加入同步队列末尾,enq()方法的返回值是node节点的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驱节点被取消,或者CAS设置其状态为SIGNAL失败了,没有前驱节点负责唤醒当前node节点了,这里直接唤醒node节点
LockSupport.unpark(node.thread);
return true;
}
2.5.2.3 signalAll()方法
signalAll()方法会将条件队列中所有节点都转移至同步队列中,具体的实现和signal()方法相类似,这里不再粘贴代码。
2.5.2.4 awaitUninterruptibly()方法
await()方法是会响应中断的,且根据中断发生在signal()信号的前后采取不同的处理措施。awaitUninterruptibly()方法是await()无视中断的版本,可以来看一下其实现:
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
我们可以看到,中断对awaitUninterruptibly()没有任何影响,但是中断标志位会被保留。
2.6 release()方法
public final boolean release(int arg) {
// 先调用tryRelease()尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 如果head节点不为null,且head节点的状态为SIGNAL或PROPAGATE,head节点有责任唤醒其后继节点
unparkSuccessor(h);
return true;
}
return false;
}
release()方法先调用tryRelease()尝试释放锁,如果成功,则根据head节点的状态考虑唤醒后继节点,否则释放锁失败。
tryRelease()方法是在ReentrantLock.Sync()方法中定义的,其逻辑如下:
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;
}
只有当state置为0,才表明锁的完全释放。
2.6 总结
通过ReentrantLock窥探AbstractQueuedSynchronizer只是管中窥豹,但也把抽象类AbstractQueuedSynchronizer具体化了,便于理解。在这一章中,我把其中和独占锁相关的东西都讲清楚了,条件队列也讲清楚了, 但共享锁还只字未提。
3. 从Semaphore的角度看AbstractQueuedSynchronizer
Semaphore,中文名又叫信号量,用以限制某一时刻同时获得锁的线程数量不能超过某一个阈值。从Semaphore的角度来看AbstractQueuedSynchronizer,我们可以看到共享锁在AbstractQueuedSynchronizer的实现形式。如果再结合之前ReentrantLock的角度,AbstractQueuedSynchronizer的全貌就展示在我们眼前了。先来看一下Semaphore的使用方法,依旧给出JDK给我们提供的一个例子:
public class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
protected Object[] items = new Object[MAX_AVAILABLE];
protected boolean[] used = new boolean[MAX_AVAILABLE];
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
public void putItem(Object x) {
if (markAsUnused(x)) {
available.release();
}
}
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (!used[i]) {
used[i] = true;
return items[i];
}
}
return null; // not reached
}
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (item == items[i]) {
if (used[i]) {
used[i] = false;
return true;
} else {
return false;
}
}
}
return false;
}
}
Pool类展示了这样一种情景,某一时刻最多只有有MAX_AVAILABLE个items中的元素被使用。在getItem()方法中我们调用acquire()方法获取到许可,在putItem()中我们调用release()方法释放许可。
Semaphore的设计理念如下:
- 一个Semaphore持有若干个许可,AbstractQueuedSynchronizer中的state变量存放的就是许可数量
- 调用acquire()方法时,会检查是否还有足够的剩余的许可,即state是否大于等于acquire()方法需要的许可数量,如果是,则获取许可成功,即获取锁成功;否则获取许可失败,即获取锁失败,此时会阻塞,即当前节点会进入同步队列中等待获取锁。
- 调用release()方法时,会将许可归还,即令state加上归还的许可数量。
- acquire()方法根据实现的不同分为公平锁和非公平锁两种。
3.1 公平锁和非公平锁
和ReentrantLock一样,Semaphore也有公平锁和非公平锁两种实现,默认的实现是非公平锁,可以通过传入一个布尔类型参数来使用公平锁的形式。公平锁由静态内部类FairSync实现,非公平锁由静态内部类NonfairSync实现,这两个类都继承了Semaphore的静态内部类Sync,而Sync继承自AbstractQueuedSynchronizer。来看一下这几个静态内部类的类图,会发现和ReentrantLock中的一模一样,连名字都一样:
NonfairSync和FairSync唯一的区别就在于tryAcquireShared()方法的实现:
// 非公平锁
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
// 公平锁
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors()) // 和非公平锁的唯一区别就是在这里去判断是否有其余节点在同步队列中等待
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
tryAcquireShared()的返回值有如下几种含义:
- 负数表示获取共享锁失败。
- 0表示获取锁成功,但是后续线程无法再获取共享锁了。
- 正数表示获取锁成功,且后续线程还可以再继续获取共享锁。
我们可以看到,对于公平锁,在尝试获取共享锁时会调用hasQueuedPredecessors()方法去查看同步队列中是否有节点正在等待获取锁,如果有线程正在等待获取锁,根据先来后到原则,当前线程还没有获取锁的资格,直接返回-1表示获取锁失败。
3.2 通过一个小case来理清楚共享锁相关方法
假设某个时刻Semaphore持有的state状态为0,线程t1和t2调用acquireUninterruptibly()方法来尝试获取许可,这两个线程都会调用acquireShared()方法,我们来看一下这个方法的实现。
3.2.1 acquireShared()方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
acquireShared()方法的逻辑很简单,首先调用tryAcquireShared()去尝试获取共享锁,tryAcquireShared()方法是在子类NonfairSync和FairSync中定义的。由于此时state为0,线程t1和线程t2都会获取许可失败,都会调用doAcquireShared()方法去获取共享锁,来看一下这个方法的实现:
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) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 和acquireQueued()方法唯一不同之处
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 线程t1和线程t2都阻塞在这里
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
显然线程t1和线程t2都会阻塞在上述parkAndCheckInterrupt()方法里。此时同步队列的状态如下:
这个时候我们调用release()方法来释放一些许可,会调用releaseShared()方法。
3.2.2 releaseShared()方法
假设线程t0调用releaseShared()一次性释放了2个许可。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
其中tryReleaseShared()方法在子类Semaphore.Sync中实现:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
如果调用tryReleaseShared()设置state成功,我们调用doReleaseShared()方法,唤醒同步队列中的节点,此时state被置为2。
3.2.2.1 doReleaseShared()方法
private void doReleaseShared() {
for (;;) { // 死循环
Node h = head;
// 如果h为null,说明同步队列还没有被初始化,没有任何节点需要唤醒
// 如果h和tail相等,说明同步队列中所有节点已经被唤醒了,没有任何节点需要唤醒
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 如果CAS设置头节点状态为0失败了,说明此时头节点状态不是SIGNAL,跳过后续唤醒后继节点的操作
// 为什么这里会CAS失败呢?在后续会有解释
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
// 如果h的状态为0,且CAS设置其状态为PROPAGATE失败了,说明在这个判断执行前后,有新节点入队,将头节点的
// 状态设置成了SIGNAL,此时我们需要进入下一轮循环
// 值得注意的是,将节点状态置为PROPAGATE的地方只有这一处,说明只有头节点才可能处于PROPAGATE状态
// 为什么这里要设置为PROPAGATE状态,设置为SIGNAL可以吗?
// 如果设置为SIGNAL,代表当前线程有必要去唤醒后继节点,第二轮循环时会进入if (ws == Node.SIGNAL)中,
// 如果CAS操作成功,仅会唤醒后续一个节点。而设置了PROPAGATE状态,在跳出该死循环后。我们可以看到在
// setHeadAndPropagate()方法中,只要头节点小于0(PROPAGATE是-3),会唤醒后续所有节点
continue; // loop on failed CAS
}
// 如果head节点在本轮循环中没有发生改变,跳出循环
// 如果head节点在本轮循环中发生了改变,说明已经有新的唤醒的节点占领了head,继续下一轮循环
if (h == head) // loop if head changed
break;
}
}
对于t1线程,我们能够通过unparkSuccessor(h)语句来将其唤醒。唤醒t1线程后,t1线程会继续doAcquireShared()方法中的死循环进入到setHeadAndPropagate()方法,我们来看一下这个方法的逻辑:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// propagate是当前state的状态,如果大于0,还有可以唤醒后继节点取获取锁
// 如果原head节点h为null,即队列为空,或者h的状态小于0,那么有责任去唤醒后继节点
// 如果新的head节点为null,即队列为空,或者新的head节点的状态小于0,也有责任去唤醒后继节点
// 为什么需要对原head节点和新head节点都做上述判断呢?
// 原head节点要出队了,自然要唤醒后继节点。
// 新head节点是新成为头节点的共享节点,也是需要唤醒后继节点的。
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// s可能为null,这是由节点的入队方式决定的,在addWaiter()方法里有讲解过
// s是null或者s是共享节点都需要去尝试唤醒后继节点
if (s == null || s.isShared())
doReleaseShared();
}
}
仔细观察我们会发现setHeadAndPropagate()方法是doAcquireShared()方法和acquireQueued()方法的唯一不同之处。因为对于独占锁而言,当前线程获取锁成功之后是没有必要去唤醒后继节点的。但是共享锁不一样,如果当前线程获取锁成功,说明当前的共享锁是处于可以被获取的状态,那么就需要唤醒后继节点去尝试获取共享锁。我们看到线程t1会调用doReleaseShared()方法去唤醒后继节点。
执行releaseShared()方法的线程t0在执行到doReleaseShared()方法中第一轮循环的if (h == head)判断时:
- 如果此时t1已经成为了同步队列的头节点,那么该判断会是false,此时线程t0会进入下一轮循环。当t0第二轮循环时走到if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))这里,而线程t1调用doReleaseShared()也会走到if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))这里,此时两个线程可能发生CAS竞争,其中某个线程会竞争失败。实际上,即使此时break跳出死循环,因为线程t1已经被唤醒成为同步队列的头节点,线程t1在setHeadAndPropagate()方法里会去负责唤醒线程t2,其实这一步操作没有也可以,只是为了提高一下唤醒线程的效率,即提高锁的吞吐量。
- 如果此时t1还没有成为同步队列的头节点,那么该判断会是true,跳出死循环,doReleaseShared()方法执行结束。
注意,在上述doReleaseShared()方法中,并没有对state状态进行判断,该操作是在doAcquireShared()中进行的。因为并不是唤醒的节点就能够获取到共享锁,只是有机会去争锁,所以没必要在唤醒的时候对state状态做一个判断。
3.3 总结
Semaphore中还有响应中断的acquire()版本,但大同小异,这里就不再分析了。至此,我们已经描述清楚了AbstractQueuedSynchronizer的全貌。接下来对CountDownLatch、CyclicBarrier、ReentrantReadWriteLock的理解会轻松许多,我们只需要关注于如何利用AbstractQueuedSynchronizer来实现这些工具类,而无需关注AbstractQueuedSynchronizer内部的实现原理。
4. CountDownLatch源码分析
CountDownLatch中的state代表现在还有多少任务没有完成。CountDownLatch初始化的时候会设置state的值。调用await()方法的线程会一直阻塞。countDown()会使state减1,当state减为0时,该线程还有额外的责任,就是去唤醒因为调用await()方法而阻塞的线程。先来看一个JDK自带的一个例子:
class Driver {
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
doSomethingElse(); // don't let run yet
startSignal.countDown(); // let all threads proceed
doSomethingElse();
doneSignal.await(); // wait for all to finish
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}
所有的Worker都在等待着主线程的startSignal.countDown()信号,因此所有的Worker都一同开始工作。而主线程调用doneSignal.await()方法在等待着所有的Worker任务完成。
Sync是CountDownLatch的一个静态内部类,继承自AbstractQueuedSynchronizer,实现了tryAcquireShared()和tryReleaseShared()方法:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
我们看到,只有当state为0时,tryReleaseShared才会返回true,此时才会去唤醒因为调用await()方法而阻塞的线程。await()方法就是将当前节点加入同步队列中。
5. CyclicBarrier源码分析
CountDownLatch的缺点是只能使用一次,且在await()唤醒等待的线程时不能执行给定的任务,CyclicBarrier就是为了解决这个问题而诞生的。不同的是,CountDownLatch基于AbstractQueuedSynchronizer的共享锁模式实现,而CyclicBarrier基于AbstractQueuedSynchronizer的Condition条件队列实现。先来看JDK自带的一个例子了解一下CyclicBarrier的使用方法。
class Solver {
final int N;
final float[][] data;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) { myRow = row; }
public void run() {
while (!done()) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
Runnable barrierAction = new Runnable() { public void run() { mergeRows(...); }};
barrier = new CyclicBarrier(N, barrierAction);
List<Thread> threads = new ArrayList<Thread>(N);
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i));
threads.add(thread);
thread.start();
}
// wait until done
for (Thread thread : threads)
thread.join();
}
}
在CyclicBarrier的构造方法中传入两个参数N和barrierAction代表当前CyclicBarrier调用N次await()方法后会执行barrierAction。先来看一下CyclicBarrier的几个关键属性:
// 可重入锁
private final ReentrantLock lock = new ReentrantLock();
// 和lock关联的条件队列,所有调用await()方法的线程都会阻塞等待在这个条件队列里
private final Condition trip = lock.newCondition();
// parties表示的意思是,在一个generation中,只有调用了parties次await()方法,barrierCommand(如果有的话)才会被执行,且
// 唤醒因调用await()方法而阻塞的所有线程
private final int parties;
// barrierCommand表示的是在一个generation中,调用了parties次await()方法之后需要执行的指令
private final Runnable barrierCommand;
// generation标记一个代
private Generation generation = new Generation();
// count表示在当前generation中,还需要调用count次await()方法,barrierCommand(如果有的话)才会被执行,接着唤醒因调用
// await()方法而阻塞的所有线程
private int count;
初始化一个generation时,count的值为parties,当有parties个线程调用await()方法时,最后一个线程会调用nextGeneration()方法开启一个新的generation,我们来看一下nextGeneration()方法的逻辑:
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
在nextGeneration()方法中,我们唤醒在条件队列trip中等待的所有线程,并重置count值为parties,开启新的一个generation。正是因为有generation这个概念的存在,CyclicBarrier才实现了多次复用。根据条件队列的性质,只有持有锁的线程才能调用signalAll()方法,同理,只有持有锁的线程才能调用nextGeneration()方法。
当在执行barrierCommand的过程中抛出异常,或者线程发生中断时,或者调用条件队列trip的awaitNanos()方法等待超时时,将会调用breakBarrier()方法来打破当前栅栏,我们来看一下breakBarrier()方法的逻辑:
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
我们可以看到,在打破栅栏函数breakBarrier()中,将当前generation标记为broken,重置count值为parties,同时唤醒条件队列trip中等待的所有线程。和nextGeneration()方法一样,调用breakBarrier()方法时也需要持有锁。
CyclicBarrier的await()方法,其实最终调用的是dowait()方法,我们来看一下dowait()方法的逻辑,这是CyclicBarrier中最核心的方法:
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
// 如果当前generation已经broken,直接抛出BrokenBarrierException异常
throw new BrokenBarrierException();
if (Thread.interrupted()) {
// 如果发生线程中断,打破栅栏,抛出InterruptedException异常
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
// 如果当前线程调用await()方法后,count值变为了0,说明该线程有执行barrierCommand和开启新的generation的责任
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 如果ranAction为false,说明在执行barrierCommand时发生了异常,打破当前栅栏
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
// 如果没有设置时间限制,调用条件队列trip的await()方法阻塞等待
trip.await();
else if (nanos > 0L)
// 如果有设置最大等待时间,调用条件队列trip的awaitNanos()阻塞等待
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
// 如果在等待过程中发生了中断,且还未开启下一个generation,且当前generation不是broken的,需要打破当前
// generation,抛出InterruptedException异常。
breakBarrier();
throw ie;
} else {
// 如果在等待过程中发生了中断,且开启了新的generation,此时没有必要再抛出InterruptedException异常,
// 记录下中断标志位即可。
// 如果当前generation已经被标记为broken,我们也只需设置一个中断标记位,让其在后续代码中抛出
// BrokenBarrierException异常。
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
// 正常最后一个线程调用nextGeneration()方法时都会开启新的一个generation,因此会在这里直接return。
// 当然如果是因为抛出异常走到的这里,或者说超时走到的这里,还没有开启一个新的generation,就不会return,而是会
// 走到上述g.broken的逻辑里抛出BrokenBarrierException异常,或者走到后续超时的逻辑里抛出TimeoutException异常
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
6 ReentrantReadWriteLock源码分析
6.1 概述
ReentrantReadWriteLock是基于AbstractQueuedSynchronizer实现的读写锁,写锁和读锁都是可重入的,但写锁是排它锁,读锁是共享锁。如果某个线程持有了写锁,那么其它线程既不能获取写锁也不能获取读锁,但已经持有写锁的线程可再次获取写锁,也可获取读锁,这就是锁降级。如果某个线程持有了读锁,那么其它线程可以获取读锁,但是不能获取写锁,即使是已经持有读锁的线程也无法获取写锁,即不支持锁升级。我们可以先来看一下JDK给我们提供的2个例子,第一个例子是和锁降级相关的缓存读取策略:
public 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();
}
}
}
在上述缓存读取策略中,我们首先获取到读锁,如果缓存未过期,则直接使用缓存值。如果缓存已过期,我们需要更新缓存值,此时释放读锁,获取写锁(这里一定要先释放读锁,因为不释放读锁是不能获取写锁的)。在更新完缓存值之后,我们获取读锁释放写锁,即执行锁降级策略。
第二个例子是针对读多写少的场景,对JDK提供的原生的集合类TreeMap做性能优化,使其适应读多写少的场景:
public class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String[] allKeys() {
r.lock();
try {
return m.keySet().toArray();
} finally {
r.unlock();
}
}
public Data put(String key, Data value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
}
使用ReentrantReadWriteLock对TreeMap进行改造,使其成为一个线程安全的类。在读取数据时加读锁,在修改数据时加写锁。相比读写都用ReentrantLock加锁而言,在读多写少的场景下能支撑起更高的吞吐量,有更好的性能。
6.2 公平锁和非公平锁的区别
ReentrantReadWriteLock的读写锁分别对应两个静态内部类:ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock。根据ReentrantReadWriteLock是否是公平锁,这两个静态内部类分别基于NonfairSync和FairSync实现。和ReentrantLock一样,NonfairSync和FairSync都继承自Sync,而Sync继承自AbstractQueuedSynchronizer。
我们先来看一下公平锁和非公平锁的实现区别,即NonfairSync和FairSync的区别:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 对于非公平锁而言,写锁永远不需要阻塞,永远会参与争锁
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 对于非公平锁的读锁,为了防止出现写线程饥饿的现象,如果同步队列中第一个等待的节点尝试获取的是写锁,则读线程需要阻塞,让位
// 给第一个尝试获取写锁的线程,apparentlyFirstQueuedIsExclusive()函数的实现在AbstractQueuedSynchronizer中,具体
// 实现细节见下面。
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// 看着判断逻辑这么复杂,其实就是去判断一下同步队列中第一个等待的节点(即头节点head的next),持有的线程是否尝试获取写锁。
// 如果是,则返回true;否则,返回false。
return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;
}
// 对于公平锁,无论是读线程还是写线程,都需要乖乖排队,即调用hasQueuedPredecessors()函数去判断同步队列中是否有等待的线程。
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
6.3 Sync的设计思想
Sync继承自AbstractQueuedSynchronizer,其state是一个32位的int型数据,我们将其划分为2半,其中高16位用于代表读线程的重入次数(1个读线程重入2次和2个读线程各重入一次,state的高16位是相同的),而低16位用于代表写线程的重入次数(由于写锁是排他锁,所以只有可能是单个线程多次重入造成state的低16位增加)。
为了提高性能,Sync记录了第一次获取读锁的线程(成员变量firstReader)、firstReader的重入次数(firstReaderHoldCount)、上一个获取读锁的线程的线程id和重入次数(包装在类HoldCounter中,记录在成员变量cachedHoldCounter中,作为缓存)和当前线程获取读锁的线程的线程id和重入次数(包装在一个ThreadLocal中,该ThreadLocal持有的是一个HoldCounter,记录在成员变量readHolds中)。
6.3.1 排它锁的实现
6.3.1.1 排它锁的获取
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
// 1. 如果没有线程持有写锁,只有线程持有读锁,显然获取失败,因为写锁是排它锁
// 2. 如果有线程持有写锁,但是持有写锁的线程不是当前线程,显然也获取失败,因为写锁是排它所
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 如果写锁的重入次数过大,抛出Error
throw new Error("Maximum lock count exceeded");
// 走到这里,说明没有线程持有读锁,有线程持有写锁,且持有写锁的线程是当前线程,因为不存在多线程竞争修改state的问题,不需要
// 使用CAS操作,直接调用setState()方法即可。
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
// 1. 写线程应当阻塞,如果是公平锁,同步队列里有等待的线程,就会满足和这个条件
// 2. CAS修改state状态失败,说明竞争写锁失败
return false;
setExclusiveOwnerThread(current);
return true;
}
6.3.1.2 排它锁的释放
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;
}
排它锁的释放逻辑很简单,因为不存在线程竞争问题。
6.3.2 共享锁的实现
6.3.2.1 共享锁的获取
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,并置firstReaderHoldCount为1
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 如果当前线程和firstReader是同一个线程,令firstReaderHoldCount加1即可
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 如果未命中缓存cachedHoldCounter,则调用readHolds.get()初始化cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 如果命中了缓存cachedHoldCounter,但是其count为0,需要设置进readHolds,
// 因为readHolds代表的是当前线程获取读锁的线程的线程id和重入次数
readHolds.set(rh);
// 走到这里,rh已经是当前代表的已经是当前获取读锁的线程的状态,将其count值加1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
// fullTryAcquireShared()和tryAcquireShared()方法的逻辑大致相同,但是其在一个死循环内获取共享锁
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
// 如果获取写锁的线程不是当前线程,获取失败,返回-1
return -1;
// 走到这里,说明是已经持有写锁的线程在尝试获取读锁,即和锁降级相关
} else if (readerShouldBlock()) {
// 如果当前获取读锁的线程需要阻塞等待,我们可能需要去更新readHolds的状态。
// 为什么是可能呢?因为如果当前线程就是firstReader,那就不需要去更新readHolds的状态,
// 因为firstReader和firstReaderHoldCount已经记录好了
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
// 如果没有命中缓存cachedHoldCounter,从readHolds.get()中去获取
rh = readHolds.get();
if (rh.count == 0)
// 这里如果count属性为0,说明是刚被初始化的,是会将其移除出readHolds的
readHolds.remove();
}
}
if (rh.count == 0)
// 如果当前线程重入的count数为0了,说明当前线程之前没有获取过读锁,
// 且当前线程又需要阻塞,自然就获取失败了,直接返回-1
return -1;
}
// 如果走到了这里,说明虽然需要阻塞,但是当前线程之前已经持有了读锁,现在只不过是重入而言,是不需要阻塞的
}
// 后续的获取逻辑和tryAcquireShared()函数基本相同
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;
}
}
}
6.3.2.2 共享锁的释放
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
// 如果firstReaderHoldCount值为1,当前线程释放读锁后,没有线程再持有读锁,firstReader置为null
firstReader = null;
else
// 只需要将firstReaderHoldCount减1即可,表示firstReader的重入次数减1
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 如果没有命中缓存cachedHoldCounter,从readHolds中获取(即新建)
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 如果count小于等于1,直接调用readHolds.remove()方法移除
readHolds.remove();
if (count <= 0)
// 走到这里,说明当前线程之前根本就没有持有读锁,因为只要持有读锁,其count值至少为1,
// 直接抛出unmatchedUnlockException异常
throw unmatchedUnlockException();
}
--rh.count;
}
// 死循环释放共享锁
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
在Sync中除了上述tryAcquire()、tryRelease()、tryAcquireShared()和tryReleaseShared()方法外,还提供了tryWriteLock()方法和tryReadLock()方法,这两个方法是非阻塞的,不会调用writerShouldBlock()和readerShouldBlock()方法,其余实现和tryAcquire()方法与tryAcquireShared()方法相同。
6.4 总结
ReentrantReadWriteLock的实现比较复杂,AbstractQueuedSynchronizer中排它锁和共享锁的获取和释放都提供了实现。且其state状态的定义比较巧妙,但在理解了AbstractQueuedSynchronizer中的方法的实现后再来看ReentrantReadWriteLock的实现细节,也并不是很难。
7 写在最后
本文先从ReentrantLock和Semaphore两个角度讲解清楚了排它锁和共享锁在AbstractQueuedSynchronizer中的实现,通过ReentrantLock的讲解也讲清楚了条件队列的概念。更进一步,分析了CountDownLatch、CyclicBarrier和ReentrantReadWriteLock三个工具类的源码。本文主要写给自己分析总结看,也与读者共勉。