AQS概述
AbstractQueuedSynchronizer是Java用于替代 Synchronized+内置等待通知(wait/notify)+内置条件队列的抽象队列同步器,该同步器管理锁,条件变量(状态变量),条件谓词三元关系,从而技术上实现了锁,条件队列,等待通知,阻塞等同步语义。在JUC中广泛使用,其中有ReentrantLock,ReentrantReadWriteLock,Semaphore,CountDownLatch,ThreadPoolExecutor#Worker,而这些基石又组成了部分并发集合,可见其重要性,该同步器比内置的伸缩性和容错性更好,并且功能比内置的更加强大,文章主要分析AQS API设计,以及如何使用该类实现自定义的锁和同步器。
AQS API一览
AQS API主要分为以下几类,1 public final 方法 ,用于实现类调用以完成获取锁/释放锁的操作,2 protected final方法,用于实现类获取,原子修改状态变量, 3 protected方法,用于实现类覆写,并且协同 protected final从而真正完成等待/通知的同步语义, 4 私有方法,作为内部实现,并非API,故不分析私有方法。
public final 方法
1 | public final void acquire(int arg) { |
基本获取/释放方法包含了以tryXXX开头的方法,这些方法都需要实现类自己来定义,通过对tryXXX方法覆写,从而实现自定义的获取释放操作。
protect方法
tryAcquire, tryRelease,isHeldExclusively是实现独占语义需要覆写的方法,而tryAcquireShared,tryReleaseShared是实现共享语义需要覆写的方法,其内部实现均为throw new UnsupportedOperationException();简单而言,就是通过状态变量的修改来决定获取锁成功,获取锁失败被阻塞,释放锁失败,释放锁成功唤醒被阻塞线程的简单语义。本质是Synchronized+wait+notify+条件队列语义的高级实现。
1 | protected boolean tryAcquire(int arg) true,成功获取,false,失败获取,线程将入队阻塞。 |
当理解了protect的语义后,就需要在protect中调用protect final来真正操作状态变量了。
protect final 方法
1 | protected final int getState() 获取状态 |
AQS使用实战
当我们实现一个锁或者同步器时候,最重要的思考是你的状态变量是什么?条件谓词是什么?状态变量和条件谓词之间的转换关系?首先应该清晰理解你需要被AQS管理的状态,其次是这些状态之间转换。可以说,状态变量及其转换带来的同步语义是最重要的设计思考。我们先从官方API实例Mutex 和BooleanLatch说起,然后深入JDK例子CountDownLatch,ReentrantLock,Semaphore,最后总结实现AQS的模板。
Mutex锁实现
互斥锁是最经典的锁,同一时刻只能有一个线程获取锁,并且不可重入。我们可以以0为释放,1为获取作为状态,当获取锁时候,将状态从0置为1,新的线程再次获取时候,将被阻塞。当释放锁时候,将状态从1置为0,并且唤醒之前被阻塞的线程。
1 状态是什么? 是否获取锁
2 状态转换? 获取锁时候,状态从0修改为1,释放锁时候,状态从1修改为0.
3 实现细节? 实现Lock接口,内部静态final类实现Sync,用于实现AQS的protected方法 ,公共方法调用AQS的public final方法。
我们来看实现:
1 | public class Mutex implements Lock, java.io.Serializable { |
BooleanLatch 同步器实现
布尔Latch,可以来回切换,只允许一个信号被唤醒,但是是共享获取的,所以使用tryAcquireShared,tryReleaseShared.
1 状态是什么?获取成功或者失败
2 状态转换? 成功1,失败-1
3 实现细节?
1 | public class BooleanLatch { |
CountDownLatch同步器实现
1 状态是什么? 当前计数值
2 状态转换?每次减少一个计数值,直到0,才进行唤醒,当计数器大于0的时候,一直等待计数器降为0
3 实现细节?共享获取,
1 | //构造函数初始化内部同步器的计数值 |
在EffectiveJava3的item17中有句话点评到:构造器应该创建完全初始化的对象,并且建立起所有约束关系。CountDownLatch是可变的,但是它的状态被刻意设计的非常小,比如创建一个实例,只能用一次,一旦定时器的计数达到0,就不能再用了。
ReentrantLock锁实现
1 状态是什么?获取锁操作次数
2 状态转换是什么?同一个线程多次获取锁,累加锁操作次数,对应的多次释放锁,减少锁操作次数
3 实现细节?实现Lock接口,独占锁
1 | //抽象同步器,设计为静态类,作为公平同步器和非公平同步器的父类 |
由此我们可以看到,可重入锁的最大次数是int最大值,也就是2147483647 ,同一个线程最大可以递归获取锁21亿次。
Semaphore同步器实现
1 状态是什么?当前可用许可数量
2 状态切换? 每当有一个线程获取到许可时候,就将许可减1,当许可减低为0的时候,阻塞线程,直到许可大于0
3 实现细节?可共享获取
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
我们可以看到,Semaphore是初始化N个许可,线程无需等待,然后每一个线程会消耗信号量,当消耗完时,会阻塞后面线程,而CountDownLatch是初始化N个计数器,然后线程等待,当计数器降为0的时候,唤醒初始化等待的线程,这两者有些相反的含义在里面。两种同用共享获取方式,共享释放释放。
4 总结
在实现锁或者同步器时候,需要思考以下几点:
1 状态变量以及状态变量的转换
2 是独占的还是共享的
当想明白以上两个问题时候,就可以动手实现你要的同步器的,一般是以内部静态类的方式继承AQS的protected方法,在protected方法中,调用protected final方法,然后在你要公共API中调用你的内部同步器的public final方法既可。如下实现模板:
1 | public MyLock implements Lock, or MySync { |
Done!