整个AQS框架核心功能都是围绕着其32位整型属性state进行,一般可以说它表示锁的数量,对同步状态的控制可以实现不同的同步工具,例如闭锁、信号量、栅栏等等。为了保证可见性此变量被声明为volatile,保证每次的原子更新都将及时反映到每条线程上。而对于同步状态的管理可以大体分为两块,一是独占模式的管理,另外是共享模式的管理。通过对这两种模式的灵活变换可以实现多种不同的同步器,如下图,对state的控制可以看成一个管道,管道的大小决定了同时通过的线程,独占模式好比宽度只容许一个线程通过的管道,在这种模式下线程只能逐一通过管道,任意时刻管内只能存在一条线程,这便形成了互斥效果。而共享模式就是管道宽度大于1的管道,可以同时让n条管道通过,吞吐量增加但可能存在共享数据一致性问题。(注意:两种模式的讨论忽略了队列的管理逻辑,实际上CLH Node的引入是为了优化竞争带来的性能问题,不影响同步状态管理的探讨)
图2-5-9-5 独占模式与共享模式
如何通过state实现独占模式和共享模式?在此之前先了解AQS框架中相关的getState、setState、compareAndSetState三个操作state的基本方法,前两个方法是普通的获取设置方法,其必须保证不存在数据竞争的情况下使用,compareAndSetState方法则提供了CAS方式的硬件级别的原子更新。两种模式就是通过这些方法对state操作实现不同同步模式,下面给出最简单的实现。
独占模式
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
setState(0);
return true;
}
多条线程通过tryAcquire尝试把state变量改为1,由于CAS算法的保证,最终有且仅有一条线程成功修改state,修改成功的线程代表获取锁成功,将拥有往下执行的权利,进入管道。当执行完毕退出管道时执行tryRelease尝试把state变量改为0,让出管道,此处由于不存在线程竞争所以可直接使用setState,接着其他未通过的线程继续重复尝试。
共享模式
public int tryAcquireShared(int interval) {
for (;;) {
int current = getState();
int newCount = current - 1;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int interval) {
for (;;) {
int current = getState();
int newCount = current + 1;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
与独占模式不同的是对state的管理及判断条件,独占模式state的值只能为0或1,而共享模式的state是可以被出事换成任意整数,一般初始值表示提供一个同时n条线程通过的管道宽度,这样一来,多条线程通过tryAcquireShared尝试将state的值减去1,成功修改state后就返回新值,只有当新值大于等于0才表示获取锁成功,拥有往下执行的权利,进入管道。在执行完毕时线程将调用tryReleaseShared尝试修改state值使之增加1,表示我已经执行完了并让出管道的通道供后面线程使用,需要说明的是与独占模式不同,由于可能存在多条线程并发释放锁,所以此处必须使用基于CAS算法的修改方法,修改成功后其他线程便可继续竞争锁。
ASQ框架提供了对同步状态state的基本操作,了解了两种模式对state操作开发者可能很自由地自定义自己的同步器。实际中AQS框架在提供state状态管理接口的同时也将维护等待队列的工作,两项工作被封装成一个模板,规定了工作流程,工作流程包括什么条件下加入等待队列、什么条件移除等待节点、如何操作等待队列、需不需要阻塞、支不支持中断等等,对外仅仅提供state状态操作接口供开发者自定义,而队列的维护工作已经绑定在模板中,无需你自己动手。