Java并发框架——AQS之如何使用AQS构建同步器

时间:2021-03-19 16:49:30

AQS的设计思想是通过继承的方式提供一个模板让大家可以很容易根据不同场景实现一个富有个性化的同步器。同步器的核心是要管理一个共享状态,通过对状态的控制即可以实现不同的锁机制。AQS的设计必须考虑把复杂重复且容易出错的队列管理工作统一抽象出来管理,并且要统一控制好流程,而暴露给子类调用的方法主要就是操作共享状态的方法,以此提供对状态的原子性操作。一般子类的同步器中使用AQS提供的getState、setState、compareAndSetState三个方法,前两个为普通的get和set方法,要使用这两个方法必须要保证不存在数据竞争,compareAndSetState方法提供了CAS方式的硬件级别的原子更新。对于独占模式而言,锁获取与释放的流程的定义则交给acquire和release两个方法,它们定义了锁获取与释放的逻辑,同时也是提供给子类获取和释放锁的接口。它的执行逻辑可以参考前面的“锁的获取与释放”,它提供了一个怎样强大的模板?由下面的伪代码可以清晰展示出来,请注意tryAcquire和tryRelease这两个方法,它就是留给子类实现个性化的方法,通过这两个方法对共享状态的管理可以自定义多种多样的同步器,而队列的管理及流程的控制则不是你需要考虑的问题。

① 锁获取模板

if(tryAcquire(arg)) {

    创建node

    使用CAS方式把node插入到队列尾部

    while(true){

    if(tryAcquire(arg) 并且 node的前驱节点为头节点){

把当前节点设置为头节点

    跳出循环

}else{

    使用CAS方式修改node前驱节点的waitStatus标识为signal

    if(修改成功)

        挂起当前线程 

}

}

② 锁释放模板

    if(tryRelease(arg)){

唤醒后续节点包含的线程

}





我们可以认为同步器可实现任何不同锁的语义,一般提供给使用者的锁是用AQS框架封装实现的更高层次的实现,提供一种更加形象的API让使用者使用起来更加方便简洁,而不是让使用者直接接触AQS框架,例如,ReentrantLock、Semphore、CountDownLatch等等,这些不同的形象的锁让你使用起来更好理解更加得心应手,而且不容易混淆。然而这些锁都是由AQS实现,AQS同步器面向的是线程和状态的控制,定义了线程获取状态的机制及线程排队等操作,很好地隔离了两者的关注点,高层关注的是场景的使用,而AQS同步器则关心的是并发的控制。假如你要实现一个自定义同步装置,官方推荐的做法是将集成AQS同步器的子类作为同步装置的内部类,而同步装置中相关的操作只需代理成子类中对应的方法即可。往下用一个简单的例子看看如何实现自己的锁,由于同步器被分为两种模式,独占模式和共享模式,所以例子也对应给出。





① 独占模式,独占模式采取的例子是银行服务窗口,假如某个银行网点只有一个服务窗口,那么此银行服务窗口只能同时服务一个人,其他人必须排队等待,所以这种银行窗口同步装置是一个独占模型。第一个类是银行窗口同步装置类,它按照推荐的做法使用一个继承AQS同步器的子类实现,并作为子类出现。第二个类是测试类,形象一点地说,有三位良民到银行去办理业务,分别是tom、jim和jay,我们使用BankServiceWindow就可以约束他们排队,一个一个轮着办理业务而避免陷入混乱的局面。

public class BankServiceWindow {

private final Sync sync;

public BankServiceWindow() {

sync = new Sync();

}

private static class Sync extends AbstractQueuedSynchronizer {

public boolean tryAcquire(int acquires) {

if (compareAndSetState(0, 1)) {

setExclusiveOwnerThread(Thread.currentThread());

return true;

}

return false;

}

protected boolean tryRelease(int releases) {

if (getState() == 0)

throw new IllegalMonitorStateException();

setExclusiveOwnerThread(null);

setState(0);

return true;

}

}

public void handle() {

sync.acquire(1);

}

public void unhandle() {

sync.release(1);

}

}





public class BankServiceWindowTest {

   public static void main(String[] args){

   final BankServiceWindow bankServiceWindow=new BankServiceWindow();

   Thread tom=new Thread(){

   public void run(){

   bankServiceWindow.handle();

   System.out.println("tom开始办理业务");

   try {

   this.sleep(5000);

   } catch (InterruptedException e) {

   e.printStackTrace();

   }

   System.out.println("tom结束办理业务");

   bankServiceWindow.unhandle();

   }

   };

   Thread jim=new Thread(){

   public void run(){

   bankServiceWindow.handle();

   System.out.println("jim开始办理业务");

   try {

   this.sleep(5000);

   } catch (InterruptedException e) {

   e.printStackTrace();

   }

   System.out.println("jim结束办理业务");

   bankServiceWindow.unhandle();

   }

   };

   Thread jay=new Thread(){

   public void run(){

   bankServiceWindow.handle();

   System.out.println("jay开始办理业务");

   try {

   this.sleep(5000);

   } catch (InterruptedException e) {

   e.printStackTrace();

   }

   System.out.println("jay结束办理业务");

   bankServiceWindow.unhandle();

   }

   };

   tom.start();

   jim.start();

   jay.start();

    }

}

输出结果如下:

tom开始办理业务

tom结束办理业务

jim开始办理业务

jim结束办理业务

jay开始办理业务

jay结束办理业务

明显tom、jim、jay仨是排队完成的,但是无法保证三者的顺序,可能是tom、jim、jay,也可能是tom、jay、jim,因为在入列以前的执行先后是无法确定的,它的语义是保证一个接一个办理。如果没有同步器限制的情况,输出结果将不可预测,可能为输出如下:

jim开始办理业务

jay开始办理业务

tom开始办理业务

jay结束办理业务

jim结束办理业务

tom结束办理业务





② 共享模式,共享模式采取的例子同样是银行服务窗口,随着此网点的发展,办理业务的人越来越多,一个服务窗口已经无法满足需求,于是又分配了一位员工开了另外一个服务窗口,这时就可以同时服务两个人了,但两个窗口都有人占用时同样也必须排队等待,这种服务窗口同步器装置就是一个共享型。第一个类是共享模式的同步装置类,跟独占模式不同的是它的状态的初始值可以*定义,获取与释放就是对状态递减和累加操作。第二个类是测试类,tom、jim和jay再次来到银行,一个有两个窗口甚是高兴,他们可以两个人同时办理了,时间缩减了不少。

public class BankServiceWindows {

private final Sync sync;

public BankServiceWindows(int count) {

sync = new Sync(count);

}

private static class Sync extends AbstractQueuedSynchronizer {

Sync(int count) {

setState(count);

}

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;

}

}

}

}





public void handle() {

sync.acquireShared(1);

}





public void unhandle() {

sync.releaseShared(1);

}





}





public class BankServiceWindowsTest {

public static void main(String[] args){

final BankServiceWindows bankServiceWindows=new BankServiceWindows(2);

Thread tom=new Thread(){

public void run(){

bankServiceWindows.handle();

System.out.println("tom开始办理业务");

try {

this.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("tom结束办理业务");

bankServiceWindows.unhandle();

}

};

Thread jim=new Thread(){

public void run(){

bankServiceWindows.handle();

System.out.println("jim开始办理业务");

try {

this.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("jim结束办理业务");

bankServiceWindows.unhandle();

}

};

Thread jay=new Thread(){

public void run(){

bankServiceWindows.handle();

System.out.println("jay开始办理业务");

try {

this.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("jay结束办理业务");

bankServiceWindows.unhandle();

}

};

tom.start();

jim.start();

jay.start();

}

}

可能的输出结果为:

tom开始办理业务

jay开始办理业务

jay结束办理业务

tom结束办理业务

jim开始办理业务

jim结束办理业务

tom和jay几乎同时开始办理业务,而jay结束后有空闲的服务窗口jim才过来。

这节主要讲的是如何使用AQS构建自己的同步器,并且剖析了锁获取与释放的模板的逻辑,让你更好理解AQS的实现,最后分别给出了独占模式和共享模式的同步器实现的例子,相信你们搞清楚这两种方式的实现后,要构建更加复杂的同步器就知道力往哪里使了。

喜欢研究java的同学可以交个朋友,下面是本人的微信号:

Java并发框架——AQS之如何使用AQS构建同步器