AQS详解
一、介绍
AQS的全称是 AbstractQueuedSynchronizer,主要是用来构造同步器和锁,Java的 Juc 包中很多锁如 ReentrangLock、Semaphore,它们都有一个共同的基类,就是AQS,因为 AQS 能十分便利的搭建锁或者同步器,所以在 Java 并发编程中得以大量使用,同样的,我们可以基于 AQS 来搭建自己的锁或者同步器。
二、基本原理
AQS是使用一个 int 型的变量来代表锁的状态的:
1 private volatile int state;//共享变量,使用volatile修饰保证线程可见性
这里利用 volatile 来保证此变量的可见性,而内部则是利用双端队列来实现线程的排序,其原理示意图:
AQS 的核心思想就是 当资源没有被占用时,有线程过来就直接占用资源,而当线程发现资源已经被占用的,AQS 就利用队列去存储这些等待的进程,当资源被释放的时候,利用合适的竞争策略去竞争。
AQS支持两种资源的占有方式:
- 独占型:即资源只有由一个线程独占
- 共享型:资源可以同时由多个线程占用
这样的设计可以使 AQS 适用于不同的场景,独占式如:ReentrangLock,共享式的如:Semaphore,CountDownLatch。
AQS提供以下几种方法重写,以应对不同的需求:
protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false
protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;
protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;
protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false
protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。
如何使用?
如果我们想设计独占式的锁,就去重写 tryAcquire() 和 tryRelease()方法,想要设计共享式的锁,就去重写 tryAcquireShared() 和 tryReleaseShared(),至于线程的阻塞策略,AQS已经实现好了,无需我们去考虑,只需要简单的对信号量进行判断,进行锁的释放和加锁操作,这是典型的模版方法设计模式的应用,AQS定义好一个锁的骨架,简单的应用让子类重写就行。
三、AQS简单的实践及测试
1 public class Muxie implements Serializable { 2 private class Lock extends AbstractQueuedSynchronizer{ 3 Lock(){}; 4 private boolean isBusy(){ 5 return getState() == 1; 6 }//判断此时的 信号量是否为1 7 8 // 重写两大自定义方法 9 @Override 10 protected boolean tryAcquire(int arg) { 11 if(compareAndSetState(0,1)){ 12 setExclusiveOwnerThread(Thread.currentThread()); 13 return true; 14 } 15 return false; 16 } 17 @Override 18 protected boolean tryRelease(int arg) { 19 if(getState() == 0){ 20 throw new InvalidMarkException(); 21 } 22 setExclusiveOwnerThread(null); 23 setState(0); 24 return true; 25 } 26 } 27 // 在类中创建对象 供外部使用 28 private final Lock lock = new Lock(); 29 public void onLock(){ 30 lock.acquire(1); 31 } 32 public boolean offLock(){ 33 return lock.tryRelease(1); 34 } 35 public boolean isLock(){ 36 return lock.isBusy(); 37 } 38 }
四、深入理解
1、独占式
获取资源方法 acquire(int):
此方法是独占式时线程获取资源的顶层方法,如果获取到资源,直接返回,否则进入等待队列,此过程忽略中断的影响,获取到资源后,线程即可以执行其临界区的代码。
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
函数执行的流程:
1、tryAcquire()尝试去获取资源,如果成功则直接返回,体现了非公平锁,因为每一个线程都有一次直接尝试获取资源的机会,这时候 CLH 队列里面可能还有线程在继续等待
2、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)尝试将线程加入等待队列的尾部,并标记为独占模式(也有非独占模式的标记)
3、acquireQueued 使线程阻塞在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断过,则返回 true 否则返回 false
4、如果返回的 true 就会执行 if 语句体,线程就会调用自我中断 selfInterrupt() 来补上中断
释放资源 release(int):
此方法是独占模式下线程释放共享资源的顶层入口,他会释放指定量的资源,如果彻底释放了,它会唤醒等待队列里的其他线程来获取资源。
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) { 3 Node h = head;//找到头结点 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h);//唤醒等待队列里的下一个线程 6 return true; 7 } 8 return false; 9 }
主要是调用 tryRelease()来释放资源,并通过其返回值来确定是否资源被完全释放了,如果完全释放才会去唤醒等待进程
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
一个非常有趣的问题:如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,这时队列中的其他结点会怎么办?是不是没法再被唤醒了?
答案是YES!!!这时,队列中等待锁的线程将永远处于park状态,无法再被唤醒!!!但是我们再回头想想,获取锁的线程在什么情形下会release抛出异常呢??
- 线程突然死掉了?可以通过thread.stop来停止线程的执行,但该函数的执行条件要严苛的多,而且函数注明是非线程安全的,已经标明Deprecated;
- 线程被interupt了?线程在运行态是不响应中断的,所以也不会抛出异常;
- release代码有bug,抛出异常了?目前来看,Doug Lea的release方法还是比较健壮的,没有看出能引发异常的情形(如果有,恐怕早被用户吐槽了)。除非自己写的tryRelease()有bug,那就没啥说的,自己写的bug只能自己含着泪去承受了。
2、共享式
获取资源方法 acquireShared(int):
此方法是共享模式下线程获取资源的顶层入口。它会获取指定量的资源,获取成功则会直接返回,失败则进入等待队列,直到获取到资源为止,整个过程忽略中断:
1 public final void acquireShared(int arg) { 2 if (tryAcquireShared(arg) < 0) 3 doAcquireShared(arg); 4 }
这里的 tryAcquireShared(arg) 依然需要自定义同步器去实现,AQS模型只是把它的返回值确定了
1、tryAcquireShared(arg) 获取成功即返回正数,直接返回
2、获取失败则通过 doAcquireShared(arg) 进入等待队列,直到获取到资源为止,而中断处理也在 doAcquireShared(arg) 里
这里与独占式不同的是,在线程自己拿到资源后,还会尝试这去唤醒其他线程,如果资源还够,其他的线程也会去占用资源
释放资源 releaseShared():
这个是共享模式下释放资源的顶层入口,如果成功释放资源后,会尝试唤醒等待线程:
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) {//尝试释放资源 3 doReleaseShared();//唤醒后继结点 4 return true; 5 } 6 return false; 7 }
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
小结:
本节详解了独占和共享两种模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,相信大家都有一定认识了。值得注意的是,acquire()和acquireShared()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,相应的源码跟acquire()和acquireShared()差不多,这里就不再详解了。