AbstractQueuedSynchronizer
所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
AQS内部使用CLH算法维护等待队列,CLH锁即Craig, Landin, and Hagersten (CLH) locks。CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程须要获取锁,且不释放锁。为false表示线程释放了锁。
结点之间是通过隐形的链表相连,之所以叫隐形的链表是由于这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。
CLHLock上另一个尾指针,始终指向队列的最后一个结点。
CLHLock的类图例如以下所看到的:
当一个线程须要获取锁时,会创建一个新的QNode。将当中的locked设置为true表示须要获取锁。然后线程对tail域调用getAndSet方法,使自己成为队列的尾部。同一时候获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转。直到前趋结点释放锁。
当一个线程须要释放锁时,将当前结点的locked域设置为false。同一时候回收前趋结点。例如以下图所看到的,线程A须要获取锁。其myNode域为true。些时tail指向线程A的结点,然后线程B也增加到线程A后面。tail指向线程B的结点。然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false,它就能够获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。
获取独占锁:
AQS实现类中类似于ReentrantLock类,在获取锁的操作中最后调用的是如下方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法由具体实现类实现,顾名思义用来获取资源,在第一次获取资源失败后进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
首先看下addWaiter(Node.EXCLUSIVE):
private Node addWaiter(Node mode) {
//创建独占模式的节点
Node node = new Node(mode);
//自旋插入节点
for (;;) {
//获取尾部节点
Node oldTail = tail;
//如果尾部节点不为空
if (oldTail != null) {
//新节点的前置节点设置为原先的尾节点
node.setPrevRelaxed(oldTail);
//将新节点设置为尾节点
if (compareAndSetTail(oldTail, node)) {
//原先尾节点next节点设置为新节点
oldTail.next = node;
return node;
}
} else {
//如果没有尾节点则说明队列为空需要初始化
initializeSyncQueue();
}
}
}
如上为在等待队列中插入当前获取资源的节点。
接下来看acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
//自旋
for (;;) {
//获取当前节点的前置节点
final Node p = node.predecessor();
//如果前置节点为头节点,说明马上就轮到自己了,可以先尝试获取资源
//在头结点释放资源后并唤醒下一个节点的时候p==head成立,进入循环
//这里保证FIFO
if (p == head && tryAcquire(arg)) {
//如果获取成功,将当前节点设置为头节点
setHead(node);
p.next = null; // help GC
return interrupted;
}
//保证前置节点状态为SIGNAL
if (shouldParkAfterFailedAcquire(p, node))
//调用park方法将自己阻塞,激活后检查中断状态并与interrupted或运算
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
再看下shouldParkAfterFailedAcquire(p, node)方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前继节点的waitStatus
int ws = pred.waitStatus;
//如果ws为SIGNAL状态,表面前继节点释放资源或中断后会唤醒自己直接返回true
if (ws == Node.SIGNAL)
return true;
//如果ws>0即为取消状态,跳过此前置节点,一直往前找,直到找到waitStatus<0的节点,并将此节点设置为 //node的前置节点,同时设置此节点的后置节点为node
if (ws > 0) {
do {
//这里往前变量才能保证if (p == head && tryAcquire(arg)) 跳出循环
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前继节点的ws值设置为Node.SIGNAL,以保证下次自旋时,shouldParkAfterFailedAcquire直接返回true
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
释放独占锁:
AQS实现类中类似于ReentrantLock类,在释放锁的操作中最后调用的是如下方法。
public final boolean release(int arg) {
if (tryRelease(arg)) {
//获取头节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法由具体实现类实现,顾名思义用来释放资源,释放成功进入后面代码。
接下来看unparkSuccessor方法:
private void unparkSuccessor(Node node) {
//获取头结点waitStatus
int ws = node.waitStatus;
if (ws < 0)
//如果ws小于0,将其waitStatus置为0
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
//如果头结点的nest节点为空或者waitStatus大于0,则从尾节点从后往前查找
//搜索到等待队列中最靠前的ws值非正且非null的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
//唤醒下一个节点线程
if (s != null)
LockSupport.unpark(s.thread);
}
后继节点的阻塞线程被唤醒后,就进入到acquireQueued()的if (p == head && tryAcquire(arg))的判断中,此时被唤醒的线程将尝试获取资源。
当然,如果被唤醒的线程所在节点的前继节点不是头结点,经过shouldParkAfterFailedAcquire的调整,也会移动到等待队列的前面,直到其前继节点为头结点。
获取共享锁
AQS实现类中类似于ReentrantReadWriteLock类,在获取共享锁的操作中最后调用的是如下方法。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared方法为AQS集成类实现,获取失败后进入doAcquireShared方法:
private void doAcquireShared(int arg) {
//此addWaiter与获取独占锁中的一致,将一个共享节点放入等待队列队尾
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
//自旋操作
for (;;) {
//获取新节点前驱节点
final Node p = node.predecessor();
if (p == head) {
//如果p就是头结点进入下面逻辑
//尝试获取资源
int r = tryAcquireShared(arg);
if (r >= 0) {
//获取成功后将当前节点设为头结点,如果r>0并唤醒后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
//保证前置节点状态为SIGNAL,方法详情分析见独占锁中分析
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
上述代码中与获取独占锁逻辑大致一样,不同的地方在于setHeadAndPropagate方法,跟踪代码如下:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//将当前节点设为头节点
setHead(node);
//当前节点获取成功后返回值大于0,表面后继节点有可能获取资源成功,所以需要唤醒后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
唤醒后继节点的方法为doReleaseShared()代码如下:
private void doReleaseShared() {
//自旋进行唤醒,由于多个线程在操作故每次获取的头结点可能会变,操作完头结点没变则跳出循环
for (;;) {
//获取头结点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//ws为SIGNAL则将状态置为0
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
释放共享锁:
AQS实现类中类似于ReentrantReadWriteLock类,在释放共享锁的操作中最后调用的是如下方法。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared由实现类实现,doReleaseShared上面已经分析过了。
至此,共享模式下的资源获取/释放就讲解完了,下面以一个具体场景来概括一下:
整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源。
- A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
- B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
- C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
- A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
- C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
- B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
- C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;
- …
条件变量支持
类似于配合synchronized关键字进行线程之间调度的notify和wait方法,条件变量的signal和await方法是配合AQS实现的锁来进行线程之间的调度
不同于synchronized同时只能与一个共享变量的notify或wait方法配合使用,AQS一个锁可以对应多个条件变量
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("t1 start");
System.out.println("condition1 await");
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("t2 start");
System.out.println("condition2 await");
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 end");
lock.unlock();
}
});
t2.start();
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("t3 start");
System.out.println("condition1 singal");
condition1.signal();
System.out.println("t3 end");
lock.unlock();
}
});
t3.start();
}
如上代码定义了两个condition即条件变量,三个线程,t1,t2线程分别等待与condition1,condition2并释放锁,t3只唤醒了condition1,t1被唤醒,t2由于没人唤醒所以一直阻塞着。
上面这个例子说明了一个锁可以对应多个条件变量,条件变量由锁的newCondition方法产生,每个条件变量里面的await和signal对应wait跟notify方法,每个条件变量里面的调度各自独立互不影响。
代码lock.newCondition()其实就是新建了一个AQS的内部类ConditionObject对象,可以访问AQS内部的变量和方法。每个条件变量的内部都维护着一个条件对垒,用来存放调用条件变量的await方法时被阻塞的线程。注意这个条件队列跟AQS队列不是一回事!!!
参考文章:https://www.jianshu.com/p/0f876ead2846
https://blog.csdn.net/varyall/article/details/80317488