什么是AQS?
AQS:AbstractQueuedSynchronizer。 是一个用来构建锁和同步器的框架,许多同步类实现都继承了AQS,如常用的ReentrantLock/Semaphore/CountDownLatch…
由以上两图可以看出,基本上所有的并发工具类都实现了AQS,可见AQS的重要性
为什么需要AQS?
- 锁和协作类都有一个共同点:闸门
- ReentrantLock和Semaphore,包括CountDownLatch、ReentrantReadWriteLock都有类似“协作”或者叫“同步”的功能,其实,他们底层都用了一个共同的基类,就是AQS
Semaphore源码:
CountDownLatch源码:
ReentrantLock源码同样如此。。。
AQS的作用
- AQS是一个用于构建锁、同步锁、协作工具类的工具类。有了AQS以后,更多的协作工具类都可以很方便的被写出来了
一句话总结:有了AQS,构建携程协作类就容易多了
AbstractQueuedSynchronizer是Doug lea写的,从JDK1.5加入的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,我们用IDE看AQS的实现类源码,可以发现实现类有一下这三个核心:
- state
- 控制线程抢锁和配合的FIFO队列
- 期望协作工具类去实现的获取/释放等重要方法
state状态
源码中就是一个被volatitle修饰的int值
- 这里的state的具体含义,会根据具体实现类的不同而不同,比如在Semaphore里,它表示“剩余的许可证数量”,而在CountDownLatch里,他表示“还需要倒数的数量”
- state是volatile修饰的,会被并发的修改,所以所有修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖于j.u.c.atomic包的支持
- 在ReentrantLock中,state用来表示锁的占用情况,包括可重入计数,当state的值为0的时候,标识改Lock不被任何线程所占用
FIFO队列
- 这个队列用来存放“等待的线程”,AQS就是“排队管理器”,当多个线程挣用同一个锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚释放的锁
- AQS会维护一个等待的线程队列,把线程都放到这个队列里
- 这是一个双向形式的队列
期望协作工具类去实现的获取/释放等重要方法
- 这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义都不同
获取方法
- 获取操作会依赖state变量,经常会阻塞(比如获取不到锁)
- 在Semaphore中,获取就是acquire方法,作用是获取一个许可证
- 在CountDownLatch里,获取就是await方法,作用是“等待,知道倒数结束”。在CountDownLatch里,await会判断state是否为0,如果state不为0,线程就会陷入阻塞状态
AQS应用实例、源码解析
- 第一步:写一个类,想好协作的逻辑,实现获取/释放方法
- 第二部:内部写一个Sync类继承AbstractQueuedSynchronizer
- 第三部:根据是否独占来重写tryAcquire/tryRelease或tryAcquireShared(int acquires)和tryRelaseShared(int releases)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shared方法
CountDownLatch源码分析
- 内部类Sync继承AQS
AQS在CountDownLatch中的应用
1 构造函数
在源码中我们可以看出, 底层将count值赋给了state
2.getCount()
这个方法也只是返回了state值
3.await()
如果线程中断,则抛中断异常。然后判断tryAcquireShared返回值是否小于0,如果小于0,则执行doAcquireSharedInterruptibly方法;
这个方法意思是,如果state==0,返回1,state!=0,返回-1,按照上面的方法来看,如果state!=0,则会执行doAcquireSharedInterruptibly方法(这个方法是让线程阻塞),如果返回1,就会直接跳过,线程就会继续执行。
doAcquireSharedInterruptibly,这个方法基本作用是将当前线程放入等待队列,并且让他进入阻塞状态。
源码中的node.predecessor()方法会将线程包装成一个Node节点,这个Node节点就是等待队列中的节点,如下图:
parkAndCheckInterrupt方法会让线程阻塞:
让我们看下源码:
其中主要是park方法:
park里面有一个UNSAFE操作:
这个park是个native方法,这个操作主要就是让线程挂起,进入阻塞状态。
4.countDown()
我们来分析一下这个源码:
首先获取state值,如果state0,说明已经有人将其释放了,直接返回false;否则通过cas操作,将state-1,然后判断修改过后的state值是否为0,将结果返回,如果修改后的state0,就返回true,否则返回false。我们再看releaseShared这个方法,如果tryReleaseShared方法返回的是true(state==0),执行doReleaseShared方法(这个方法主要是将线程由等待状态唤醒),否则返回false,线程继续阻塞,直到state变为0。
AQS在CountDownLatch的总结
- 调用CountDownLatch的await方法时,便会尝试获取“共享锁”,不过一开始是获取不到该锁的,于是线程被阻塞
- 而共享锁可获取的条件,就是锁计数器的值为0,而锁计数器的初始值为count,每当一个线程调用该CountDownLatch对象的countDown方法时,才将锁计数器-1
- count个线程调用countDown之后,锁计数器才为0,而前面提到的线程获取共享锁的线程才能继续运行