Java并发编程之AQS学习

Posted by Lucky Xue on 2020-01-31

学习AQS的思路

AbstractQueuedSynchronizer是Doug Lea写的,简称AQS。从JDK 1.5加入的一个基于FIFO等待队列用于实现同步器的基础框架,学习AQS的目的主要是为了理解原理、提高技术以及应对面试。

先从应用层面理解为什么需要它、如何使用它;然后再看一看我们JUC的设计者是如何使用它来了解它的应用场景;最后我们再去分析它的结构,这样的话我们就学习的更加轻松了。

我们已经学过了ReentrantLock和Semaphore,有没有发现它们有共同点?很相似?事实上,不仅是ReentrantLock和Semaphore,包括CountDownLatch、ReentrantReadWriteLock都有这样类似"协作"(或者叫"同步")功能,其实,它们底层都用了一个共同的基类,这就是AQS,锁和协作类的共同点:闸门。

ReentrantLock、CountDownLatch和Semaphore等内部有一个Sync类,Sync类继承了AQS。

AQS是一个用于构建锁、同步器、协作工具类的工具类(框架),有了AQS,更多的协作工具类都可以很方便地被写出来。一句话总结:有了AQS,构建线程协作类就容易多了。

AQS的比喻:

Semaphore:一个人面试完了以后,后一个人才能进来继续面试;

CountDownLatch:群面,等待10个人到齐;

Semaphore和CountDownLatch这些同步工具类,要做的就是写在自己要人的规则。比如是"出一个,近一个"或者说"凑齐10个,一起面试";

剩下的招呼面试者的脏活累活交给AQS来做;

continuous_deployment

AQS内部原理解析

AQS最核心的就是三大部分:

  • state;

  • 控制线程抢锁和配合的FIFO队列;

  • 期望协作工具类去实现的获取/释放等重要方法;

state状态

这里state的具体含义,会根据具体实现类的不同而不同,比如在Semaphore里,它表示"剩余的许可证的数量";而在CountDownLatch里,它表示"还需要倒数的数量";在ReentrantLock中用来表示锁的占有情况,包括可重入计数。当state的值为0的时候,标明该Lock不被任何线程所占有;
state是volatile修饰的,会被并发地修改,所以所有修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖juc.atomic包的支持;

控制线程抢锁和配合的FIFO队列

这个队列用来存放"等待的线程",AQS就是"等待线程",AQS就是排队管理器,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁;
AQS会维护一个等待的线程队列,把线程都放到这个队列里,这是一个双向形式的队列;

continuous_deployment

期望协作工具类去实现获取/释放等重要方法

这里的获取和释放方法是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同;
获取方法:获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候);

释放方法:释放操作不会阻塞,在Semaphore中,释放就是release方法,作用是释放一个许可证;在CountDownLatch里面,释放就是countDown方法,作用是倒数1个数;还需要重写tryAcquire和tryRelease等方法;

AQS的用法

第一步:写一个类,想好协作的逻辑,实现获取/释放方法;
第二步:内部写一个Sync类继承AbstractQueuedSynchronizer;
第三步:根据是否独占来重写tryAcquire/tryRelease或tryAcquireShared(int acquires)/tryReleaseShared(int releases)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shared方法;
AQS在CountDownLatch的应用总结

调用CountDownLatch的await方法时,便会尝试获取"共享锁",不过一开始是获取不到该锁的,于是线程被阻塞;

而"共享锁"可获取到的条件,就是"锁计数器"的值为0;

而"锁计数器"的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将"锁计数器"减一;
经过count个线程调用CountDown()之后,"锁计数器"才为0,而前面提到的等待获取共享锁的线程才能继续执行;
AQS在Semaphore的应用

在Semaphore中,state表示许可证的剩余数量;

判断tryAcquire方法,判断nonfairTryAcquireShared大于等于0的话,代表成功;

这里会先检查剩余许可证数量够不够这次需要的,用减法来计算,如果直接不够,那就返回负数,表示失败。如果够了,就用自旋加compareAndSetState来改变state状态,知道改变成功返回正数;或者是期间如果被其他人修改了导致剩余数量不够了,那也返回负数代表获取失败;

AQS在ReentrantLock的应用

分析释放锁的方法tryRelease,由于是可重入的,所以state代表重入的次数,每次释放锁,先判断是不是当前持有锁的线程释放的,如果不是就抛出异常,如果是的话,重入次数就减一,如果减到了0,就说明完全释放了,于是free就是true,并且把state设置为0;

加锁的方法,判断当前state是否等于0,也会判断当前线程是不是当前持有锁的线程,如果都不是说明目前拿不到这把锁,就会被放在等待队列中,后续会在合适的时机被唤醒;

基于AQS实现一个一次性Latct门闩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
* 描述:自己用AQS实现一个简单的线程协作器
*/
public class OneShotLatch {

private final Sync sync = new Sync();

public void signal() {
sync.releaseShared(0);
}
public void await() {
sync.acquireShared(0);
}

private class Sync extends AbstractQueuedSynchronizer {

@Override
protected int tryAcquireShared(int arg) {
return (getState() == 1) ? 1 : -1;
}

@Override
protected boolean tryReleaseShared(int arg) {
setState(1);

return true;
}
}


public static void main(String[] args) throws InterruptedException {
OneShotLatch oneShotLatch = new OneShotLatch();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"尝试获取latch,获取失败那就等待");
oneShotLatch.await();
System.out.println("开闸放行"+Thread.currentThread().getName()+"继续运行");
}
}).start();
}
Thread.sleep(5000);
oneShotLatch.signal();

new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"尝试获取latch,获取失败那就等待");
oneShotLatch.await();
System.out.println("开闸放行"+Thread.currentThread().getName()+"继续运行");
}
}).start();
}
}

AQS补充材料

以下文章是关于AQS源码分析的,通常并不要求掌握,我找了一些优质学习资源,提供给小伙伴参考:
美团技术团队《从ReentrantLock的实现看AQS的原理及应用》

老钱《打通 Java 任督二脉 —— 并发数据结构的基石》

HongJie《一行一行源码分析清楚AbstractQueuedSynchronizer》

爱吃鱼的KK《AbstractQueuedSynchronizer 源码分析 (基于Java 8)》

waterystone《Java并发之AQS详解》

英文论文的中文翻译

AQS作者的英文论文

并发编程参考书籍

continuous_deployment

关注【憨才好运】微信公众号,了解更多精彩内容⬇️⬇️⬇️

continuous_deployment