AQS
AQS全名叫做AbstractQueuedSynchronizer,从名字可以看出,它是一个抽象的队列型的同步器。被广泛地用于比如需用并发控制的类中。AQS分为两种:独占式的和共享式的。ReentrantLock就是独占式的,CountDownLatch就是共享式的。两者的区别就是独占式的,同一时刻只能有一个线程可以持有资源,其他线程竞争资源会阻塞;而共享式的,同一时刻可以有多个线程持有资源,当资源不够时,其他竞争资源才会阻塞
AQS主要通过下面几个属性以及数据结构来完成并发控制
// 代表资源状态
private volatile int state;
// 等待队列的尾节点
private transient volatile Node tail;
// 等待队列的头结点
private transient volatile Node head;
// 队列中的每个节点分为两种类型,SHARED对应共享式的,EXCLUSIVE对应独占式的
// 并且每个节点具有多个状态:CANCELLED,SIGNAL,CONDITION,PROPAGATE
// 其中需要注意,只有CANCELLED状态的值是小于0的
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
//........
}
其实,AQS就是使用一个int变量state代表资源的状态,一个双向链表组成的FIFO队列来控制并发
自定义AQS
如果我们想要完成自己的并发控制,那么我们只需要实现如下几个简单的方法即可
AQS使用模板设计模式,我们只需要指明根据状态什么情况下能够成功申请资源,什么情况下能够成功释放资源即可,其他的比如申请资源失败之后的处理方法,由AQS操作
如果需要自定义独占式的并发控制,那么我们需要实现下面两个方法:
// 尝试获取资源,返回值代表操作是否成功
boolean tryAcquire(int arg)
// 尝试释放资源,返回值代表操作是否成功
boolean tryRelease(int arg)
如果需要自定义共享式的并发控制,那么我们需要实现下面两个方法:
// 代表获取完资源后,资源的剩余量
// 如果小于零,代表获取失败
int tryAcquireShared(int arg)
// 释放资源,返回值代表操作是否成功
bool tryReleaseShared(int arg)
独占式源码分析
独占式主要调用acquire()方法和release()方法,下面我们分别分析它们的源码
public final void acquire(int arg) {
// 首选调用tryAcquire()方法,判断能否获取到资源
// 如果能够获取到资源,那么函数就结束了,接着线程就可以执行自己的任务
// 如果获取资源失败,那么就会调用addWaiter()方法,创建一个代表当前线程的节点
// 添加到等待队列末尾,然后执行acquireQueued()方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 能够运行到这里,代表首次竞争任务失败,从而线程阻塞,添加到等待队列中
// 在阻塞的过程中被中断了,但是因为线程在阻塞状态下无法响应中断,所以需要在
// 获取资源能够正常运行任务时,再执行中断响应
selfInterrupt();
}
// addWaiter的实现十分简单,就是创建一个代表当前线程的节点,然后使用cas添加到等待队列的末尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 尾节点不为null,代表当前队列中有等待任务
// 尝试使用cas将当前节点设置为尾节点
if (pred != null) {
node.prev = pred;
// 在并发情况下,这里可能会失败
// 如果设置成功就直接返回
// 如果设置失败,会通过后面的enq方法,保证设置成功
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当前队列中没有等待任务或者有等待任务但是首次cas失败
enq(node);
return node;
}
private Node enq(final Node node) {
// 这里使用死循环和cas保证设置成功
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;
}
}
}
}
至此,任务添加到等待队列就分析完了,接下来分析一下任务添加到等待队列之后需要接着做什么,看acquireQueued()
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);
// 方便gc回收节点
p.next = null; // help GC
failed = false;
// 返回当前节点的任务在阻塞过程中是否被中断过
return interrupted;
}
// 判断当前任务是否能够安全地进入阻塞状态
// 如果能那么就进入阻塞状态,死循环就不会继续执行了,暂停在这里了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取当前节点前一个节点的状态
int ws = pred.waitStatus;
// 如果前一个节点的状态是signal,那么就判断为可以进入阻塞状态
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果前一个节点的状态大于0,也就是CANCENLLED
// 那么当前节点会沿着链表向头结点方向移动,知道找到一个节点的状态<=0,将当前节点连接在该节点的后面
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 其实就是将中间那些CANCELED的节点从队列中移除
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 将当前线程阻塞
// 当一个线程被唤醒之后,就会从这个方法中退出,并且返回在阻塞的时候是否被中断过
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
至此,对acquire的分析就结束了,总结一下流程:
- 尝试获取资源,如果资源获取成功,那么就可以继续执行自己的任务;
获取失败,创建一个代表当前线程的节点,添加到阻塞队列的末尾 - 在一个死循环里,判断自己是否是第二个节点,并且可以成功获取资源,如果获取成功,那么就退出死循环,执行自己的任务
不是第二节点或者获取资源失败,那么就判断自己能够安全地阻塞(即找到一个前面的节点,节点状态是SIGNAL的,连接到该节点的后面),如果能够安全地阻塞,那么就阻塞,等待唤醒
下面分析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;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从队列的末尾向前遍历,唤醒离head最近的没有cancel的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
共享式源码分析
public final void acquireShared(int arg) {
// 返回值小于0代表资源不够
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 整体的逻辑和独占式的很像,不同点在于,当当前节点成功获取资源后,还会唤醒之后的节点
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);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
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);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
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() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 获取当前头结点的状态
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒之后的节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
唤醒多个节点的示意图如下
当head节点成功获得资源后,如果资源还有剩余,会唤醒第二个节点,并且将第二节点设置为head节点,并且唤醒第三个节点
就是通过这种方式,唤醒多个节点的
下面看一下释放资源
逻辑也很简单,如果成功释放资源,那么就唤醒后面的节点
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
公平并发控制
下面以ReentrantLock来分别介绍公平和不公平并发控制
在创建ReentrantLock时,可以传入一个参数来指明是公平的还是不公平的
这里的公平和非公平指的是,线程获得资源的顺序和线程申请获得资源的顺序相同
假如A线程当前持有资源,B线程请求资源失败,加入了等待队列
当A线程释放资源时,B被唤醒,同时另外一个线程C也请求资源
如果能够保证B获得资源,那么就是公平的,否则就是不公平的
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面分析FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前状态
// state == 0 代表资源可用,state == 1代表资源不可用
int c = getState();
// 当前资源可用
if (c == 0) {
// 判断等待队列中是否还有等待任务,如果有等待任务,那么当前线程直接添加到等待队列中,如果没有等待任务,那么设置state,并且设置当前线程拥有资源
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;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 返回true的情况,等待队列中有阻塞任务
// 且
// 只有一个等待任务或者头节点的下一个节点不代表当前线程
// 也就是说下一个可能获得资源的节点不是当前节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
```
一个新的线程只有在没有等待的任务时才能使用资源,如果队列中有等待任务,那么自己也添加到等待队列中
## 不公平并发控制
```java
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 如果能够将状态设置为1,那么直接就可以拥有资源,不用理会等待任务
// 所以是不公平的
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 当申请资源失败时,会执行下面的代码
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// c == 0代表资源可用
if (c == 0) {
// 粗暴地尝试将状态设置为1来占用资源
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;
}
```
每当一个新线程申请资源,都会不理会等待队列中是否有等待任务,直接使用cas尝试占用资源,这就是不公平的