并发之AQS的双向链表

谈谈 AQS

  AQS(AbstractQueuedSynchronizer)JUC包下的一个抽象类。虽然是抽象类,但没有抽象方法,即便子类集成,也无法直接使用锁功能。AQS中关于锁的判断TryAcquireTryRelease方法,默认都是报错,需要子类集成后进行重写,才能使用锁功能。
  JUC包的一些并发功能都是基于AQS实现的,例如:ReentrantLockThreadPoolExecutor、并发集合等。
  AQS的实现包含:一属性,两队列。
   一属性:state。表示锁的状态。初始化为0表示无锁,获取锁后+1,释放锁时-1。一般在TryAcquireTryRelease中实现+、-功能。
  两队列:双向队列与单向队列。

双向队列

双向链表,也称作等待队列、阻塞队列。是在线程获取锁失败后的等待处理操作。AQS中存在<Node> head<Node> tail属性用于组成双向队列。
重点关注的几个重点词:

  1. AbstractQueuedSynchronizer存在headtail属性,所以其本身就是一个链表。并没有使用集合

  2. 双向链表(等待队列)

    1. head 永远都是伪节点(thead = null)
    2. tail 初始化时是伪节点(初始化时, head == tail),之后就不是了。
  3. node的作用就是封装线程信息,然后并放到链表中排队

  4. node节点有5种状态:

    1. 用于双向链表(CANCELLED、SIGNAL、0)
    2. 用于单向链表(CONDITION、CANCELLED)
    3. 用于共享锁(PROPAGATE)
  5. 双向链表中节点状态

    1. tail的节点状态永远是 0
    2. head初始化为0,之后变为-1
    3. 中间节点的状态为 -1
    4. 被取消(无效/中断)的节点状态为 1。
  6. 挂起线程: LockSupport.pack(thread)

  7. 唤醒线程: LockSupport.unpack(thread)

  8. 获取锁操作:
       acquire -> tryAcquire -> addWaiter -> acquireQueued(死循环)
       -> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt -> LockSupport.park
       -> setHead(可以认为,删除唤醒节点)

  9. 获取锁异常操作:
       cancelAcquire

  10. 取消锁操作:
       release -> tryRelease -> unparkSuccessor -> LockSupport.unpark

  11. 双向链表中是否可以不用head节点?
      可以不用。

    1. 在设计之前就提出了伪节点的存在,
    2. head节点的使用可以简化
  12. 为什么是双向链表,而不是用单线链表?
      因为在使用单线链表时,删除中间节点时,无法将node.prev.next 指向node.next。解决方法只能不断遍历,增加了很多无用操作。
      而使用双向链表就没有这个问题

锁的获取与释放 - 获取锁代码步骤

1. acquire 开启锁入口

  所有调用锁功能的入口

public final void acquire(int arg) {
    // 尝试获取锁
    if (!tryAcquire(arg) &&
            // 没有拿到锁,走此方法
            // addWaiter(Node.EXCLUSIVE) 封装层一个node节点, 并放入到双向链表中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 当前线程中断
        selfInterrupt();
}

2. tryAcquire 尝试获取锁

  尝试获取锁。需要子类方法去重写此类。返回true,表示获取到锁,返回false。表示未获取到锁,代码才会往后走。
大体逻辑

  1. CAS,如果AQS.state = 0,则将 AQS.state = 1
  2. 修改 aqs.thread = 当前线程

  AQS中对于TryAcquire需要子类重新,这里,我们使用ThreadPoolExecutor.WorkerTryAcquire方法(主要是代码简单)

protected boolean tryAcquire(int unused) {
    // cas 方式获取锁,就是尝试修改 state 参数
    if (compareAndSetState(0, 1)) {
        // 获取到锁(独占锁),将方法头 markwork 中线程改为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

3. addWaiter 将线程放入等待队列中

  没有获取到锁,创建一个node节点,并放入双向队列的中,队列遵循FIFO。enq()是head与tail初始化方法。可以细看一下,代码简单,在这里不做描述。

private Node addWaiter(Node mode) {
    // 创建一个节点,设置其同步队列中的下一个节点。
    // 此处可以看出,node其实就是线程的封装
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果tail != null,说明队列中有值,将node放入到队列的末尾
    if (pred != null) {
        node.prev = pred;
        // cas的经典实用
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果 tail 为null时,说队列没有值,需要初始化 head 与 tail
    // 以死循环的方式,一定将node放入到队列的末尾
    enq(node);
    return node;
}

4. acquireQueued(死循环) 将刚加入线程,挂起

  方法体中存在死循环,线程被挂起后,基本上两圈半后,就会返回 true,结束循环。

  • 第一圈,将前置节点状态改为 -1。
  • 第二圈,将线程挂起。
  • 半圈,线程被唤醒后成为head,变为伪节点。

  而被唤醒的线程一定是Head.next节点。当然如果,线程本身就是head.next,则直接过去到锁。不会挂起。

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取node的前置节点
                final Node p = node.predecessor();
                // 如果 node.pre节点,也就是 p == head,说明node就是第一顺位节点。
                // 这样,就尝试获取锁资源
                // 如果不是,就放到队列里面
                if (p == head && tryAcquire(arg)) { // tryAcquire方法,会被不同的子类重写,可以查看 ReentrantLock、ThreadPoolExecutor.Worker
                    // 获取锁资源成功,node就是变成 head,也变成 伪节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire(p, node)
                // 功能:将 p节点状态改为 -1,并返回false。当返回true后,才会执行 parkAndCheckInterrupt方法,挂起
                // 所以这个死循环,一般会走两边,
                // 第一次循环,将p节点状态改为 -1
                if (shouldParkAfterFailedAcquire(p, node) &&
                        // 第二次循环,将 node 节点挂起。LockSupport.pack(this)
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 在 acquireQueued 基本上不会走这个方法
            if (failed)
                cancelAcquire(node);
        }
    }

5. shouldParkAfterFailedAcquire 挂起前准备,将前置节点状态改为 -1

  将 node.prev节点状态改为 -1,并返回false。当返回true后,才会执行 parkAndCheckInterrupt方法,挂起。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // ws == Node.SIGNAL 表示 pred节点可以唤醒后面的节点
        // 只有 ws = -1,node才会挂起,并返回 true
        if (ws == Node.SIGNAL) return true;
        // ws > 0 ,只会是 1,表示此节点已被取消。
        if (ws > 0) {
            // 当前node节点的 pre节点的pre节点,作为 node的pre节点
            // 知道找到 pred.waitStatus != 1的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * cas 将 pred 节点的状态改为 -1。表示可以唤醒后续线程
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

6. parkAndCheckInterrupt 挂起线程(LockSupport.park)

  挂起线程,查看线程的唤醒,是因为异常报错,还是正常唤醒。

private final boolean parkAndCheckInterrupt() {
    // 挂起线程方法
    LockSupport.park(this);
    // 这个方法可以确认,当前线程是中断唤醒的,还是正常唤醒的
    return Thread.interrupted();
}

7. setHead 被唤醒的节点,变为head(可以认为,删除唤醒节点)

  能走到setHead方法,说明已经获取锁。并将node代替head节点。
  需要注意的事:head.next 不能设置为空。释放锁时用。

image

锁的获取与释放 - 释放锁代码步骤

1. release 释放锁入口

  释放锁的两个动作:1.将AQS.state--,2.唤醒 head.next线程

public final boolean release(int arg) {
    // tryRelease 如果锁释放,则返回true,否则false
    if (tryRelease(arg)) {
        // 锁已经放掉,走此逻辑
        Node h = head;
        // h != null,说明有排队的
        // h.waitStatus != 0,说明有排队的,并且线程已经挂起
        if (h != null && h.waitStatus != 0)
            // 唤醒下一个线程等待节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

2. tryRelease 尝试释放锁

  与TryAcquire相同,需要子类重写此方法。
大体逻辑:

  1. 判断 AQS.state == 0。只有等于0,才说明完全释放锁,才会有后面动作。大于0,说明锁未完全释放。
  2. 设置 AQS.thread =null

  为了能体现,AQS.state > 0的情况,我们查看ReentrantLockTryRelease方法

protected final boolean tryRelease(int var1) {
    // 获取 AQS.state值。var1必然是1
    int var2 = this.getState() - var1;
    // 如果当前线程不是 拥有锁的线程,则报错
    if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    } else {
        boolean var3 = false;
        // 判断 AQS.state == 0
        if (var2 == 0) {
            var3 = true;
            // AQS.thread =null
            this.setExclusiveOwnerThread((Thread)null);
        }
        // 如果是重入锁,此时调整锁的次数
        // 实际上就是 state--
        this.setState(var2);
        return var3;
    }
}

3. unparkSuccessor 唤醒下一个节点 LockSupport.unpark(head.next.thread);

  唤醒 head.next线程。被唤醒的节点,会继续执行acquireQueued的方法。
  假如head.next.waitStatus =1,也就是说,下一个节点被取消了,不能唤醒。则tail上前寻找距离head最近的有效节点,并唤醒。
  通过acquireQueued方法,可以知道被唤醒的节点最终会成为head

 private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    // ws<0,则 ws =1 或者 =-1
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 假如 s 节点被中断了。是null
    // 将从 tail 向前找,找到距离head最近的有效节点,并赋值给s,进行唤醒
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果不是 null,则将直接唤醒线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

锁的获取与释放 - 获取锁异常代码步骤

1. cancelAcquire 取消在AQS(双向链表)中的node

  代码一般被写到finally块内,保证获取锁异常时一定执行
取消节点的操作流程:

  1. 将node.thread = null
  2. 往前找到有的节点作为node.prev
  3. 将 node.waitStatue = 1,表示节点取消
  4. 将node 脱离 双向链表,分三种情况
    4.1 当node 是tail,直接删除
    4.2 当node 是 head.next,删除 并唤醒下一个节点
    4.3 当node 是中间节点,删除 并确保node.prev.waitStatus一定为-1
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;
    // 1. 将 thread 设置为 null
    node.thread = null;

    // 2. 往前找到 节点状态有效的节点,就是 !=1
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    // 3. node节点状态,改为已删除
    node.waitStatus = Node.CANCELLED;

    // 4.1 如果node节点是 tail,则将 prev 作为 tail节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // 将 tail.next = null
        // 感觉没有必要,直接 == null,就可以了
        // compareAndSetTail可以保证,下一步操作的唯一
        compareAndSetNext(pred, predNext, null);
    } else {
        // 代码走到这,说明node不是 tail
        int ws;
        // 4.3 node是中间节点,确保prev节点状态是-1,这样才可以指向next节点
        if (pred != head &&
                // 下面代码: 就是确保 prev的状态一定是 -1 ,这样就可以唤醒后续节点
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                        // ws 并发情况下,有可能是0,但也要改为-1
                        (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // 二次校验,prev是一个有效节点
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                // 下面代码的意思: prev.next = node.next
                compareAndSetNext(pred, predNext, next);
        } else {
            // 4.2 如果 prev == head,说明node是head.next
            // 则直接唤醒node.next节点
            unparkSuccessor(node);
        }
        // 将自己指向自己,可以 gc 删除
        node.next = node; // help GC
    }
}
posted @ 2023-11-18 14:54  之士咖啡  阅读(214)  评论(0编辑  收藏  举报