基于核心源码和个人思考——AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

主要暴露了锁的申请和释放等方法,实现同步队列的管理。

机制

  • 资源机制
    • 独占机制:确保只有一个线程能获取到资源,无论资源是否获取完,只能有一个线程获取到(独占机制),但该线程可以在获取到资源的前提下再继续申请资源,并申请成功(重入机制)。
    • 共享锁:多个线程排队申请指定数量的资源,若剩余的资源大于申请的资源,则申请成功,下一个线程继续尝试申请(共享机制);若等于,则本线程获取资源成功,下一个线程阻塞,不尝试申请资源(限制了共享的数量);若小于,则本线程申请资源失败。如果线程申请的资源数量是资源的总额,可转化成独占机制。
  • 队列机制
    • 加入队列后,每个节点只有前驱节点是头节点(头节点即已经被唤醒在运行的节点),才有机会去获取资源。因此这个队列是先进先出的。
    • 两种资源获取机制在排队时的表现:
      • 独占机制仅在释放资源后会唤醒头节点的后继节点线程(不是唤醒被释放节点的后继节点),后继节点被唤醒后尝试去获取资源一般都会成功。所以独占机制下,每个节点只有在释放后有一次唤醒后继节点的操作。
      • 共享锁会在获取到锁资源后和释放锁资源后都会尝试唤醒头节点的后继节点。若当前节点执行过唤醒等待节点线程的操作,则设为PROPAGATE态,下次需要唤醒时遇到PROPAGATE态则不再唤醒,避免后继节点被重复唤醒。所以在共享机制下,每个节点都会有两次唤醒后继节点的操作,后继节点则有可能会被多次唤醒并尝试获取资源。

内部类Node

属性

  • 模式
    • static final Node SHARED = new Node() 共享
    • static final Node EXCLUSIVE = null 独占
  • volatile int waitStatus 等待状态
    • CANCELLED = 1 节点已取消(中断超时等)
    • INITIAL = 0 初始状态
    • SIGNAL = -1 后继节点的线程处于等待状态,表示后继节点还没有被当前线程唤醒过
    • CONDITION = -2
    • PROPAGATE = -3 标记已扩散过,即防止重复调用
  • volatile Node prev 上一个节点
  • volatile Node next 下一个节点
  • volatile Thread thread
  • Node nextWaiter 用于共享机制

接口

  • boolean isShared() 是否共享
  • Node predecessor() 获取前驱节点(对前驱节点进行封装,若为空,则抛出NullPointerException)

属性

  • volatile Node head 队列头
  • volatile Node tail 队列尾
  • volatile int state 锁状态,为0时为无锁
  • long spinForTimeoutThreshold = 1000L 阻塞阈值:超过这个阈值则阻塞,未超过则自旋

接口

锁状态接口

  • int getState()
  • void setState(int newState)
  • compareAndSetState(int expect, int update) 用CSA尝试对锁状态赋值。

队列接口

  • Node enq(final Node node) 自旋插入队尾(若队列未初始化则先初始化)

    while true
        t = 队尾
        if t为空
            if CSA(对头, null, node)  # 对头直接是new Node()
                队尾 = 对头
        else
            t = 队尾
            if CSA(队尾, t, node)
                node的前继节点设为t
                t的后继节点设为node
                return t
    复制代码
  • Node addWaiter(Node mode) 队尾添加一个等待者

    根据模式生成一个节点node  # 独占或共享
    pred = 队尾
    if 队尾不为空
        node的前继节点设为pred
        if CSA(队尾, pred, node)
            pred的前继节点设为node
            return node
    初始化队列并自旋插入node到队尾  
    return node
    复制代码
  • void setHead(Node node) 设置对头

  • void unparkSuccessor(Node node) 唤醒后继节点

    ws = 当前节点的等待状态
    if ws < 0
        CSA(当前节点状态, ws, 0)  # 等待执行态
    s等于node后面的节点中,离node最近且不是Cancel状态的节点
        若去不到,s设为空
    if s不为空
        激活s线程。
    复制代码
  • void doReleaseShared() 释放共享锁

  • void setHeadAndPropagate(Node node, int propagate) 设置头节点,并唤醒后续节点

private void setHeadAndPropagate(Node node, int propagate) { 
    Node h = head; 
    setHead(node);
    /*
    5个条件:
        propagate > 0:propagate是剩余资源,只有大于0时才继续唤醒后续线程。
        h == null:表示此节点变成头节点之前,同步队列为空,现在当前线程获得了资源,那么后面共享的节点也可能获得资源
        h.waitStatus<0:如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了资源有剩余,所以需要唤醒后继共享模式节点,由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断。
    */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
复制代码
  • void cancelAcquire(Node node) 取消获取锁操作(把节点设为cancel态)

    if node是空
        return
    pred = node的前继节点
    while pred的等待态是Cancel
        node的前驱 = pred = pred的前驱
    把node的等待态设为Cancel
    if node是队尾 且 CSA(队尾, node, pred) # 这里主要是用于判断node是不是队尾,CSA执行失败不影响,其他方法会有类似剔除cancel态节点的操作。
        CSA(pred的后继节点, node, null) # 在添加队尾的时候有重定向后继指针的操作,会覆盖错误的指向。
    else
        if pred不是对头,且pred的线程不为空,且pred等待态为Signal或者CSA等待态不为cancel并CSA替换为Signal成功
            if node后继节点不为空 且 等待态不为cancel
                CSA(pred后继节点, node, node的后继节点)
        else
            唤醒node的后继节点。
    复制代码
  • boolean shouldParkAfterFailedAcquire(Node pred, Node node) 获取锁失败即将进入阻塞态(没有阻塞,只是做阻塞前的准备):确保前驱节点是signal态,并把前面cancel态的节点剔除

    ws = pred的等待态
    if ws是Signal
        return true
    else if ws是Cancel
        while pred的等待态是Cancel
            node的前驱 = pred = pred的前驱
        pred后继设为node
    else
        CSA(pred的等待态, ws, Signal)
    复制代码
  • void selfInterrupt() 中断当前线程

  • boolean parkAndCheckInterrupt() 阻塞,检查自我中断

    阻塞当前线程
    (被唤醒)return 检测是否有中断
    复制代码

各种获取锁的私有方法

  • boolean acquireQueued(final Node node, int arg) 尝试获取锁:尝试获取锁,失败则加入队列并阻塞

    fail = true
    try
        while true
            interrupted = false
            p = node前驱节点
            if p是头节点 且 获取锁成功
                把node设为队头
                p的后继设为空
                fail = false
                return interred
            if 阻塞当前线程准备 且 阻塞唤醒后是否被中断
                interred = true
    finally
        if fail是true
            把当前节点设为取消态。
    复制代码
  • void doAcquireInterruptibly(int arg) 获取可中断锁:

    • 主要逻辑和acquireQueue()一样,唯一区别在线程被唤醒后检测到被中断会直接抛异常InterruptedException。
  • boolean doAcquireNanos(int arg, long nanosTimeout) 获取锁,若超时则放弃。

    • 主要逻辑和acquireQueue()一样
    • 每次循环会计算剩余超时时间,超时时间超过阈值(一般是1000L毫秒),则继续阻塞剩余超时时间,小于阈值则自旋,不再阻塞。
  • void doAcquireShared(int arg) 获取共享锁

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); // 生成共享节点插入队列
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {  // 只有前驱节点是头,才能竞争锁资源
                    int r = tryAcquireShared(arg);  // 尝试申请资源,返回值的r表示剩余资源
                    if (r >= 0) {  // 申请资源成功
                        setHeadAndPropagate(node, r); 设为队头,并继续唤醒后续节点
                        p.next = null;
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
复制代码
  • void doAcquireSharedInterruptibly(int arg) 获取可中断共享锁
  • boolean doAcquireSharedNanos(int arg, long nanosTimeout) 获取共享锁,超时则放弃

主要暴露的方法和需要重写的方法

  • (待重写) boolean tryAcquire(int arg)

  • (待重写) boolean tryRelease(int arg)

  • (待重写) int tryAcquireShared(int arg) 尝试申请锁资源

    • 大于0: 申请成功,唤醒下一个等待线程竞争资源
    • 等于0:申请成功,资源耗尽,下一个线程等待资源释放。
    • 小于0:申请失败,阻塞,等待下一周期唤醒。
  • (待重写) boolean tryReleaseShared(int arg)

  • (待重写) boolean isHeldExclusively() 当前线程是否是持有该锁的线程

  • void acquire(int arg) 获取锁

    if (!tryAcquire(arg) //尝试获取锁
            && 
            acquireQueued(
                addWaiter(Node.EXCLUSIVE),  // 生成节点放入队列
                arg
            ) 排队,阻塞,阻塞结束,返回中断标志
        ) 
            selfInterrupt();
    复制代码
  • void acquireInterruptibly(int arg)

  • boolean tryAcquireNanos

  • boolean release(int arg)

  • void acquireShared(int arg)

  • void acquireSharedInterruptibly(int arg)

  • boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

  • boolean releaseShared(int arg)

  • boolean hasQueuedThreads() 是否还有线程在等待执行

  • boolean hasContended()

  • Thread getFirstQueuedThread() 获取第一个等待的线程

  • Thread fullGetFirstQueuedThread()

  • boolean isQueued(Thread thread) 指定线程是否在队列中

  • boolean apparentlyFirstQueuedIsExclusive() 排第一的等待线程申请的锁是否是独占的

  • boolean hasQueuedPredecessors() 是否有排在当前线程的前继者

  • int getQueueLength() 等待队列的长度

  • Collection getQueuedThreads() 获取等待队列所有的线程

  • Collection getExclusiveQueuedThreads() 获取等待队列所有独占的线程

  • Collection getSharedQueuedThreads() 获取等待队列所有共享的线程

相关问题

为什么acquireQueued方法和doAcquireShared方法中setHead方法不需用CSA来替换呢?

  • 在acquireQueued方法中,即独占锁环境中,因为只有队头的次节点(即当前节点的前驱是队头)才能执行到此方法,因此基本只有一个线程能执行到此方法,没有资源临界问题。
  • 在doAcquireShared方法中,应该说在doAcquireShared方法的setHeadAndPropagate的方法中,其实也不会出现多个线程调用setHead方法。虽然有可能出现多个线程同时执行完毕,并唤醒后继线程,但因为head没有随着每次次节点唤醒而立刻改变,因此只是次节点会被并发唤醒多次,即只有一个线程被唤醒,并调用setHead方法。因此setHead方法本身只会被一个线程执行。
  • 逻辑:唤醒线程 =》 setHead =》 唤醒新的线程 =》 setHead。 这个线性顺序无法被打破。

好像只调用一次acquire方法,而多次调用release方法也没有问题,但锁不是应该解锁和上锁是一对一关系吗?

  • AQS更多的是设计申请资源和释放资源的概念,而没有过于强调是谁获取了资源这个概念。
  • 资源是可以一次获取多个再分批次释放的,因此一次acquire多次release理论上行的通;而锁机制是基于获取资源和释放资源的基础上实现的,而且锁机制锁本身就是一个原子单位,且有谁持有锁的概念,因此理论上是一次上锁对应一次解锁,类似资源申请和释放的一种特殊情况。
  • 实现上,多次调用release方法,AQS是会多次去唤醒等待队列的最近等待线程去尝试获取资源;而锁一般会先判断当前线程是否是持有锁的对象,调用第一次解锁方法后,当前线程就不再是持有锁的对象,若第二次再调用,则会因不是持有锁对象又尝试解锁而抛出异常。

AQS节点的状态总结

  • 新建的节点是0,INIT态
  • 在队列等待是-1,SIGNAL态
  • 节点执行时(节点处于head时)
    • 刚被唤醒并获取到资源时,等待态是-1,SIGNAL态
    • 即将要唤醒后继节点时(一般用自旋CSA替换成功后,下一步就立刻唤醒后继节点),等待态会被切换成0。标识后继节点已被唤醒过。
  • (共享锁)节点唤醒后继节点后,等待态会从0切换成-3,PROPAGATE态,标识该节点已向后共享资源。
  • 线程节点被中断或在调度时出现异常,会被设为1,CANCEL态。

相关链接

猜你喜欢

转载自juejin.im/post/5e4e348ae51d4526de392352