Java AQS 抽象的队列式的同步器 AbstractQueuedSynchronizer 学习笔记 2022-3-23

原文地址:http://www.cnblogs.com/waterystone/p/4920797.html

AQS、抽象的队列式的同步器

AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用ReentrantLock/Semaphore/CountDownLatch

整体架构

它维护了 一个volatile int state(代表共享资源)一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
总结:整体架构由 volatile修饰的int型state变量 + FIFO线程等待队列 组成

state的访问方式有三种:

getState()
setState()
compareAndSetState()

AQS定义两种资源共享方式:

(1)独占、Exclusive(只有一个线程能执行,如ReentrantLock)
(2)共享、Share(多个线程可同时执行,如Semaphore/CountDownLatch)

不同的自定义同步器争用共享资源的方式也不同;自定义同步器在实现时只需要 实现共享资源state的获取与释放方式 即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了;

自定义同步器实现时主要实现以下几种方法:

1、isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它
2、tryAcquire(int):独占方式,尝试获取资源;成功则返回true,失败则返回false
3、tryRelease(int):独占方式,尝试释放资源;成功则返回true,失败则返回false
4、tryAcquireShared(int):共享方式,尝试获取资源;负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
5、tryReleaseShared(int):共享方式,尝试释放资源;如果释放后允许唤醒后续等待结点返回true,否则返回false

ReentrantLock

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再
tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念;但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的

CountDownLatch

以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

总结

一般来说,自定义同步器 要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可;但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

源码学习

结点状态waitStatus

Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。
变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

(1)CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化
(2)SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
(3)CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
(4)PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点
(5)0:新结点入队时的默认状态
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

acquire(int)

此方法是独占模式下线程获取共享资源的顶层入口;如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响;这也正是lock()的语义,当然不仅仅只限于lock();获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:

public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}

函数流程如下
1、调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2、没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3、acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4、如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上

由于此函数是重中之重,我再用流程图总结一下:

这也就是ReentrantLock.lock()的流程,不信你去看其lock()源码吧,整个函数就是一条acquire(1);

tryAcquire(int)

此方法尝试去获取独占资源;如果获取成功,则直接返回true,否则直接返回false;这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock();如下是tryAcquire()的源码:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

什么?直接throw异常?说好的功能呢?
好吧,还记得概述里讲的AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?就是这里了!!!
AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!
至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!
当然,自定义同步器在进行资源访问时要考虑线程安全的影响

这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。

addWaiter(Node)

此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点

private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);

    //尝试快速方式直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }

    //上一步失败则通过enq入队。
    enq(node);
    return node;
}

enq(Node):此方法用于将node加入队尾

private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

如果你看过AtomicInteger.getAndIncrement()函数源码,那么相信你一眼便看出这段代码的精华。CAS自旋volatile变量,是一种很经典的用法

......t太多了,还是慢慢看吧
https://www.cnblogs.com/waterystone/p/4920797.html

posted @   紫薇哥哥  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示