Java并发19:AQS

113 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第19天,点击查看活动详情

学习MOOC视频记录的笔记

1.学习AQS的思路

  • 学习AQS的目的主要是想理解原理、提高技术,以及应对面试
  • 先从应用层面理解为什么需要他如何使用它,然后再看一看我们Java代码的设计者是如何使用它的了解它的应用场景
  • 这样之后我们再去分析它的结构,这样的话我们就学习得更加轻松了

2.为什么需要AQS?

  • 锁和协作类有共同点:闸门
    • 我们已经学过了 ReentrantLockSemaphore,有没有发现它们有共同点?很相似?
    • 事实上,不仅是 ReentrantLockSemaphore,包括 CountDownLatchReentrantReadWriteLock 都有这样类似的"协作”(或者叫“同步”)功能,其实,它们底层都用了一个共同的基类,这就是AQS
    • 因为上面的那些协作类,它们有很多工作都是类似的,所以如果能提取出一个工具类,那么就可以直接用,对于 ReentrantLockSemaphore 而言就可以屏蔽很多细节,只关注它们自己的“业务逻辑”就可以了

SemaphoreAQS 的关系

Semaphore 内部有一个 Sync 类,Sync 类继承了 AQS

AQS 1

CountDownLatch 也是一样的,展示代码

AQS的比喻

  • 比喻:群面、单面
  • 安排就坐、叫号、先来后到等HR的工作就是AQS做的工作
  • 面试官不会去关心两个面试者是不是号码相同冲突了,也不想去管面试者需要一个地方坐着休息,这些都交给HR去做

每个面试者就是一个线程

  • Semaphore:一个人面试完了以后,后一个人才能进来继续面试
  • CountDownLatch:群面,等待10人到齐
  • Semaphore、CountDownLatch这些同步工具类,要做的,就只是写下自己的“要人”规则。比如是“出一个,进一个“或者说“凑齐10人,一起面试”
  • 剩下的招呼面试者的脏活累活交给AQS来做

如果没有AQS

  • 就需要每个协作工具自己实现
    • 同步状态的原子性管理
    • 线程的阻塞与解除阻塞
    • 队列的管理

在并发场景下,自己正确且高效实现这些内容,是相当有难度的,所以我们用AQS来帮我们把这些脏活累活都搞定,我们只关注业务逻辑就够了

3.AQS的作用

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

4.AQS的重要性、地位

AbstractQueuedSynchronizer是Doug Lea写的,从JDK1.5加入的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,我们用IDE看AQS的实现类,可以发现实现类有以下这些:

AQS 2png

5.AQS内部原理

解析

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

  • state
  • 控制线程抢锁和配合的 FIFO 队列
  • 期望协作工具类去实现的获取/释放等重要方法

5.1 state状态

  • 这里的 state 的具体含义,会根据具体实现类的不同而不同比如在 Semaphore 里,它表示“剩余的许可证的数量”,而在 CountDownLatch 里,它表示“还需要倒数的数量

  • statevolatile 修饰的,会被并发地修改,所以所有修改 state 的方法都需要保证线程安全,比如 getStatesetState 以及 compareAndSetState 操作来读取和更新这个状态。这些方法都依赖于 j.u.c.atomic 包的支持

  • ReentrantLock中,state用来表示"锁”的占有情况,包括可重入计数,当 state值为0的时候,标识该 Lock 不被任何线程所占有 【此时这个锁处于释放状态,其他线程可以来获取了】

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

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

AQS 3

head 是已经拿到锁的线程,后面的是等待获取锁的线程,现在被阻塞了。

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

  • 这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同
  • 获取方法:获取操作会依赖 state 变量,经常会阻塞(比如获取不到锁的时候)
  • Semaphore 中,获取就是 acquire 方法,作用是获取一个许可证
  • 而在 CountDownLatch 里面,获取就是 await 方法,作用是”等待,直到倒数结束
  • 释放方法:释放操作不会阻塞,在 Semaphore 中,释放就是 release 方法,作用是释放一个许可证
  • CountDownLatch 里面,获取就是 countDown 方法,作用是"倒数1个数

需要重写 tryAcquiretryRelease 等方法

6.应用实例、源码解析

6.1 AQS用法

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

内部类 Sync 继承 AQS

CountDownLatch源码分析

6.2 AQS在CountDownLatch的应用

  • 构造函数

    // 传入需要倒数的数量
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 创建内部类Sync对象
        this.sync = new Sync(count);
    }
    
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
    
        Sync(int count) {
            // 将state的值设置为count
            setState(count);
        }
    
        int getCount() {
            return getState();
        }
    
        protected int tryAcquireShared(int acquires) {
            // 如果倒数已经为0了,那么都可以放行了
            return (getState() == 0) ? 1 : -1;
        }
    
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    
  • getCount

    返回当前 state 的数值

  • countDown

    public void countDown() {
        sync.releaseShared(1);
    }
    
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 唤醒队列中的其他线程
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            // 使用CAS更新值
            if (compareAndSetState(c, nextc))
                // 这个闸门可以放开了
                return nextc == 0;
        }
    }
    
  • await

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        
        // 如果小于0,线程进入等待队列
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    private void doAcquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 将线程包装为一个Node节点,阻塞
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final AbstractQueuedSynchronizer.Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    

小结:

  • 调用 CountDownLatchawait 方法时,便会尝试获取"共享锁”,不过一开始是获取不到该锁的,于是线程被阻塞
  • 而“共享锁”可获取到的条件,就是“锁计数器”的值为 0
  • 而“锁计数器”的初始值为count,每当一个线程调用该 CountDownLatch 对象的 countDown() 方法时,才将"锁计数器"-1
  • count个线程调用countDown0之后,"锁计数器”才为0,而前面提到的等待获取共享锁的线程才能继续运行。

6.3 AQS在Semaphore的应用

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