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抛出异常呢??

  1. 线程突然死掉了?可以通过thread.stop来停止线程的执行,但该函数的执行条件要严苛的多,而且函数注明是非线程安全的,已经标明Deprecated;
  2. 线程被interupt了?线程在运行态是不响应中断的,所以也不会抛出异常;
  3. 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()差不多,这里就不再详解了。

 

 

 

 

 

 

  

 

posted @ 2022-03-08 16:03  空心小木头  阅读(1052)  评论(0编辑  收藏  举报