当我们在使用一个并发工具时,总是围绕着三个最重要的入口方法来使用的:构造方法、 acquire 获取方法、release 释放方法
- 构造方法在抽象什么呢?
细看三个实现
AQS的并发工具:ReentrantLock、CountDownLatch、Semaphore。它们的构造函数都是在做一件事,围绕构造一个同步器实例,存放于属性内部。该同步器就是对AQS的具体实现,不过它们三者都有两种实现:公平与非公平之分。
那在构造对应的同步器实例的时候, 需要用到 AQS 提供的什么功能呢?
// CountDownLatch的同步器构造函数
Sync(int count) {
setState(count);
}
// Semaphore的同步器构造函数实现
Sync(int permits) {
setState(permits);
}
从上面可以看出, ReentrantLock 直接用的就是无参构造,没有做任何操作,而另外二者都是在初始化对应的 state 状态。从而可以引出 AQS 中最重要的一个属性之一:state 表示临界资源的状态,可以根据独占式和共享式来表明不同的状态。比如:CountDownLatch 中表明对应的计数,ReentrantLock 中表示加锁的状态,比如重入次数,Semaphore 表示共享资源还可以被几个线程同时持有。接下来让我们把视角移到具体的使用。
2. acquire 方法在抽象什么?我们需要实现什么?
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从上面的代码我们可以看出,这里一共干了两件事:尝试获取锁,加入等待队列。这里的 tryAcquire就是并发容器中需要去具体实现的方法,用于对 state 进行操作并返回结果,这里 AQS 只是抽象了其逻辑,保证了执行的顺序,从而我们使用者只需要关注对应的实现逻辑即可,具体的执行流程都由 AQS 抽象出来了。
紧接着是 AQS 中具体的实现 addWaiter 和 acquireQueued,用于获取临界资源失败后到等待队列中等待的操作,而不是一直尝试自旋争抢。这里具体的实现就是围绕创建 Node 节点和将 Node 节点入队等待合适的时机由前面的 Node 对其进行唤醒。
那共享式有什么区别呢?
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
第一个操作同样也是获取共享资源并更改对应的 state ,而这里的失败与否不再是互斥关系,而可以存在多个成功的可能,失败了就直接进入等待队列排队。所以整体上并无明显区别
2. 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;
}
这里的 release 实现也是同 acquire 一样,抽象了方法的流程,具体的实现交由具体的工具进行实现。而 tryRelease 就是工具需要实现的第二个方法,在独占式中,如果锁得到了释放,那么就会通过等待队列的头节点来对下一个节点进行 unpark 唤醒,并且会涉及到下一个节点的来继续作为头节点
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
共享式和独占式几乎一致,并没有太大的差别,只需注意在共享模式下共享资源可以同时被多个线程持有,那么在等待队列中如何实现一个唤醒链呢?用 Node.PROPAGATE 来保证唤醒操作的传播成功,若节点进入了对应的唤醒逻辑,但是存在竞争被另一个线程争抢了唤醒机会,那么就需要该 Node.PROPAGATE 保证传播的有效性,防止消息丢失。
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;
}
}