1. AQS
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO的队列。底层实现的数据结构是一个双向链表
1.1 AQS 属性
- head(Node):头结点,直接把它当做 当前持有锁的线程 可能是最好理解的
- tail(Node):阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
- state(int):这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁,这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
- exclusiveOwnerThread(Thread):继承自AbstractOwnableSynchronizer,代表当前持有独占锁的线程,用来判断重入等操作
1.2 Node 属性
队列中每个线程被包装成一个 Node,它主要有以下几个属性:
- waitStatus(int):等待状态,CANCELLED = 1,代表线程取消了争抢这个锁;SIGNAL = -1,表示当前node的后继节点对应的线程需要被唤醒;CONDITION = -2;PROPAGATE = -3
- prev(Node):前驱节点的引用
- next(Node):后继节点的引用
- thread(Thread):线程本尊
- nextWaiter(Node):链接到等待条件的下一个节点,或者共享特殊值,在使用共享锁或者 Condition 时用到。因为条件队列仅在处于独占模式时才被访问,所以只需要一个简单的单链即可在节点等待条件时保存节点,然后将它们转移到队列以重新获取。由于条件只能是互斥的,因此使用特殊值来表示共享模式来保存字段
1.3 使用 AQS 时重要的方法
要使用 AQS,主要通过以下几个方法,这些方法都需要子类实现:
- tryAcquire:尝试以独占模式获取,可用于实现 Lock#tryLock 方法
- tryRelease:尝试释放独占同步资源,只有获取到同步状态的线程才能调用该方法
- tryAcquireShared:尝试以共享模式获取
- tryReleaseShared:共享模式下尝试释放同步资源
- isHeldExclusively:查看同步器是否被自己独占
2. ReentrantLock 加锁解锁流程
2.1 加锁操作
构造函数:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
构造函数中,fair 对应是否是非公平锁。
FairSync#Lock & FairSync#tryAcquire:
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
// 因为刚刚还没人的,我判断过了
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;
}
NonfairSync#Lock & NonfairSync#tryAcquire:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 设置当前拥有独占访问权限的线程。
// 一个null参数表示没有线程拥有访问权限。此方法不会强加任何同步或volatile字段访问
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;
}
公平和非公平的区别在于 lock 和 tryAcquire 的实现不同。公平锁 lock 时候直接调用 acquire 方法,而非公平锁要先 CAS 抢一下锁才调用 acquire 方法。
AbstractQueuedSynchronizer#acquire:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 返回 true 了
// 说明当前线程需要被中断,这里设置一下线程中断标记
selfInterrupt();
}
acquire 两者逻辑就相同了,先执行 tryAcquire 抢一下锁,如果抢到锁了,把自己设置为同步器的独占线程;如果抢锁失败,将当前线程封装成 node 后加入阻塞队列中等待前序节点唤醒。tryAcquire 的时候,公平锁只有在没有等待队列时才会去尝试 CAS 抢占锁,而非公平锁会直接去进行一步尝试 CAS 抢锁。
也就是说,在加入阻塞队列前,非同步锁至少要抢 2 次锁,而同步锁只有在阻塞队列为空的时候才会去抢锁,讲究先来后到。
AbstractQueuedSynchronizer#addWaiter:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果阻塞队列不为空,可以尝试将节点快速插入等待队列
// 如果阻塞队列为空或者 CAS 失败则执行常规插入(enq方法)
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 采用自旋的方式入队
enq(node);
return node;
}
addWaiter 是将当前线程封装成 Node 后加入阻塞队列中,先 CAS 尝试快速加入一下,这个时候如果阻塞队列不为空并且没有其他线程竞争加入阻塞队列,大概率是成功的;如果 CAS 失败,转而调用 enq 方法,自旋入队,这一步一只有成功后才会返回。
AbstractQueuedSynchronizer#enq:
private Node enq(final Node node) {
for (; ; ) {
// 初始化head和tail
Node t = tail;
// 队列为空也会进来这里
if (t == null) { // Must initialize
// 初始化head节点
// 原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,可能是很多线程同时进来
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
/*
* AQS的精妙就是体现在很多细节的代码,比如需要用CAS往队尾里增加一个元素
* 此处的else分支是先在CAS的if前设置node.prev = t,而不是在CAS成功之后再设置。
* 一方面是基于CAS的双向链表插入目前没有完美的解决方案,另一方面这样子做的好处是:
* 保证每时每刻tail.prev都不会是一个null值,否则如果node.prev = t
* 放在下面if的里面,会导致一个瞬间tail.prev = null,这样会使得队列不完整。
*/
node.prev = t;
// CAS设置tail为node,成功后把老的tail也就是t连接到node。
if (compareAndSetTail(t, node)) {
// 极端情况,如果线程执行完 CAS 操作后被 kill 掉,那么链表该节点前驱的后继
// 就会为 null,所以在 AQS 通知的时候是采用从后往前遍历的,这样才不会有影响
t.next = node;
return t;
}
}
}
}
成功加入阻塞队列后,就该执行 AbstractQueuedSynchronizer#acquireQueued 方法了:
final boolean acquireQueued(final Node node, int arg) {
// 标识是否获取资源失败
boolean failed = true;
try {
// 标识当前线程是否被中断过
boolean interrupted = false;
// 自旋操作
for (; ; ) {
// 获取当前节点的的前驱节点
final Node p = node.predecessor();
// 如果前继节点为头节点,说明排队马上排到自己了,可以尝试获取资源
// 若获取资源成功,则执行下述操作
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
// 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
// 所以当前节点可以去试抢一下锁
// 这里我们说一下,为什么可以去试试:
// 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
// enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
// 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
// tryAcquire 就是简单用CAS试操作一下state
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为头节点
setHead(node);
// 前继节点已经释放掉资源了,将其next置空,方便虚拟机回收
// 对于每个头节点, node.thread = null , node.prev = null
// 把 p.next 指向 null , 则之前的头节点不再含有强引用
p.next = null; // help GC
// 标识获取资源成功
failed = false;
// 返回中断标记
return interrupted;
}
/*
* 获取前继节点不是头节点或者获取资源失败
* 则需要通过 shouldParkAfterFailedAcquire 函数
* 判断是否需要阻塞该节点持有的线程
* 若 shouldParkAfterFailedAcquire 函数返回 true
* 则继续执行 parkAndCheckInterrupt() 函数
* 将该线程阻塞并检查是否可以被中断,若返回 true,则将 interrupted 标志置于 true
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 最终获取资源失败,则当前节点放弃获取资源
// 什么时候 failed 会为 true?
// tryAcquire() 方法抛异常的情况
if (failed)
cancelAcquire(node);
}
}
这个方法有个自旋操作,只有当自己是阻塞队列头节点即前驱是 head 时会去尝试抢锁,抢到锁返回中断状态,否则执行 AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire 方法,判断自己是不是应该挂起:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
/*
* 如果 ws 的值为 -1,说明前继节点完成资源的释放或者中断后,会通知当前节点的
* 回去等通知就好了,不用自旋频繁地打听消息
*/
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
/*
* 如果前继节点的 ws 值大于0,即为1,说明前继节点处于放弃状态(cancelled)
* 那就继续往前遍历,直到当前节点的前继节点的 ws 值为 0 或负数
* 直到满足 if(p == head && tryAcquire(arg)) 条件,acquireQueued方法才能够跳出自旋过程
*
* 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
* 这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
* 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
*/
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
* 将前继节点的ws值设置为Node.SIGNAL,以保证下次自旋时,
* shouldParkAfterFailedAcquire直接返回true
*
* 每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,
* 然后阻塞,等待被前驱唤醒
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.2 解锁操作
正常情况下,如果 shouldParkAfterFailedAcquire 返回 true,即需要挂起,会调用 parkAndCheckInterrupt 方法进行挂起操作,等待被唤醒
ReentrantLock#unlock
public void unlock() {
sync.release(1);
}
AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
回到 ReentrantLock.Sync#tryRelease 方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 这里就是重入问题,只有当 state == 0 了,才能释放同步资源
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease 返回 true,即完全释放掉同步资源后,调用 unparkSuccessor 唤醒阻塞队列中离头节点最近的非取消节点的线程:
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;
// 从后向前遍历,找到离node最近的非取消节点,避免在node==tail时再次有入队节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
被唤醒的线程醒来后又回到这个方法:acquireQueued(final Node node, int arg),这个时候,node的前驱是head了,它也就可以拿锁了。到这里,从加锁到解锁唤醒就闭环了。
2.3 ReentrantLock 执行过程示例
首先,假设有两个线程获取锁。第一个线程调用 reentrantLock.lock( ) 方法,此时没有任何线程占用锁,tryAcquire(1) 直接返回 true,获取到锁,结束(此时只是设置了 state =1,连 head 都没有初始化,也就没有阻塞队列)
此时状态:
+----- AQS ----+
| state(1) |
| head(null) |
| tail(null) |
| exThread(1) |
+--------------+
线程 1 没有调用 unlock( ) 之前,线程 2 调用 Lock 获取锁。线程 2 会初始化 head(new Node( ),此时 head 的 waitStatus = 0)
+----- AQS ----+ +---------+
| state(1) | head | ws(0) | tail
| exThread(1) | | th(null)|
+--------------+ +---------+
之后线程 2 也会插入到阻塞队列中挂起。线程 2 执行 shouldParkAfterFailedAcquire 方法判断自己是否需要挂起时,会将 head 的 waitStatus 设置为 -1,表示等待 head 的通知
+----- AQS ----+ +---------+ +---------+
| state(1) | head | ws(-1) |<----| ws(0) | tail
| exThread(1) | | th(null)|---->| th(2) |
+--------------+ +---------+ +---------+
线程 1 调用 unlock 释放锁,将独占线程设置为 null、state 设置为 0后,调用 unparkSuccessor 方法,从后往前遍历阻塞队列,找到离头节点最近的非取消节点的线程(线程 2)进行唤醒。
+------ AQS ------+ +---------+ +---------+
| state(0) | head | ws(-1) |<----| ws(0) | tail
| exThread(null) | | th(null)|---->| th(2) |
+-----------------+ +---------+ +---------+
唤醒的线程会继续执行 acquireQueued 方法,获取到锁后将自己设置为头节点,返回中断标记。
+----- AQS ----+ +---------+
| state(1) | head | ws(0) | tail
| exThread(2) | | th(2) |
+--------------+ +---------+
说明:
- head 一般情况下是获取到线程的节点,head 之后的队列才叫阻塞队列
- waitStatus 中 SIGNAL(-1) 状态的意思是,代表后继节点需要被唤醒。
3. CountDownLatch
CountDownLatch在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,并且其操作都是原子操作。CountDownLatch 基于 AQS 的共享模式的使用,该类主要通过countDown( )和await( )两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为 0 时,这时候阻塞队列中调用await( )方法的线程便会逐个被唤醒,从而进入后续的操作。
4. CyclicBarrier
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。CyclicBarrier字面意思是“可重复使用的栅栏”。
在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒
CountDownLatch 和 CyclicBarrier 的区别:
- CountDownLatch 基于 AQS 共享模式实现,CyclicBarrier 基于 Condition 实现
- CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值
- CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截,一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能
- 除此之外,CyclicBarrier还提供了:resert( )、getNumberWaiting( )、isBroken( )等比较有用的方法
5. Semaphore
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法,经常用于限制获取某种资源的线程数量。