java-AQS

参考: Java并发之AQS详解

Lock

使用Lock 时要显式地获取和释放锁,虽然获取锁和释放锁相比 synchronized 要麻烦,但是麻烦意味着对于所得操控更加灵活,可以可中断获取锁、超时获取锁等。。

Lock 只是一个接口, 它定义了一些获取锁和释放锁的基本操作

方法描述
void lock()获取锁,获取后返回
void lockInterruptibly()获取锁,直到获取成功或者被中断
boolean tryLock()尝试获取锁,立即返回结果
boolean tryLock(long time, TimeUnit unit)如果在指定时间内获取到了锁并没有没中断过,则返回true
void unlock()释放锁,释放或返回
Condition newCondition()获取“等待通知”组件

下面通过一个 Lock 的实现类 ReentrantLock 来分析具体方法的内部实现。

ReentrantLock

如图是 ReentrantLock 类的关系图。

我们看到 ReentrantLock 实现了 Lock 接口, 并且内部有一个 Sync 类, 而 Sync 类继承自 AbstractQueueSynchronizer

再进入 ReentrantLock 内部看看源码。

public void lock() {
    sync.lock();
}
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
public void unlock() {
    sync.release(1);
}
....

可以看出 ReentrantLock 类的锁操作方法都调用了 Sync 类的方法。 所以说,ReentrantLock 的锁操作都是由 继承了 AbstractQueueSynchronizerSync 类完成的。

AbstractQueueSynchronizer 队列同步器是用构建锁或者其他同步组件的基础框架,是实现锁的关键。在锁的实现中聚合同步器,利用同步器实现锁的语义。利用这种组合, 锁只需实现与使用者交互的接口, 而锁的实现交给内部的同步器去完成。

AbstractQueueSynchronizer 队列同步器

概览

图片来自 Java并发之AQS详解

同步器在内部使用了 private volatile int state; 变量表示同步状态, 通过内置的 FIFO 队列完成资源获取线程的排队工作。 同步器的设计是基于模板方法模式的,使用者只需继承 AbstractQueueSynchronizer 并重写相关方法,就能与自定义同步组件组合,完成自定义同步组件的实现(如:ReentrantLock).

在同步器内部的等待队列中,只有头结点是获取了同步状态的,其他的结点要么在等待,要么正在前往等待途中。

自定义的同步器需要实现以下方法:

tryAcquire(int) //获取资源
tryRelease(int) //释放资源
tryAcquireShared(int) //获取共享资源
tryReleaseShared(int) //释放共享资源
isHeldExclusively() //返回是否独占资源

AbstractQueueSynchronizer 同时实现了一些模板方法供子类继承使用,模板方法分为三类: 独占式获取-释放、共享式获取释放 和 查询同步队列中的等待情况。主要说一下独占式获取-释放。

独占式获取-释放操作是下面两个方法:

void            acquire(int arg)
boolean     release(int arg)

独占式同步状态的获取 acquire()

acquire(int arg) 源码如下

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //acquireQueued 方法在自旋获取同步状态后 返回线程是否被中断过
        //由于在获取同步状态过程中是不可被中断的, 因此需要在这里把中断状态补上
        selfInterrupt();
}

方法中调用了 tryAcquire(arg) 方法完成同步状态的获取 , 如果获取成功直接返回。从前面知道, truAcquire() 是需要子类重写的方法,因此在 AQS 类中此方法只有一行抛出异常代码:

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

主要看一下获取失败情况,获取失败后执行 addWaiter()acquireQueued() 方法。addWaiter() 完成节点的入队操作, acquireQueued() 完成节点入队后自旋检查状态的操作。

addWaiter()

private Node addWaiter(Node mode) {
    //完成队列节点构造
    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 方法中是一个自旋操作, 不断地通过 CAS 自旋操作尝试入队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //队列只有头节点情况
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //CAS 操作尝试入队
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued()

acquireQueued() 方法中也是一个自旋过程,是节点(线程)获取同步状态的核心函数

final boolean acquireQueued(final Node node, int arg) {
    //记录获取同步状态是否失败
    boolean failed = true;
    try {
        //记录是否被中断过
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); //首先获取当前节点(线程) 在队列中的前驱节点
            //如果前驱节点是头结点,说明前驱节点已经获得同步状态, 因此需要 tryAcquire()
            if (p == head && tryAcquire(arg)) {
                //获取同步状态成功,此结点成为头结点, 并返回
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /*
             * 当前驱节点不是头结点,或者获取同步失败时要执行两个方法
             * shouldParkAfterFailedAcquire 确定是否需要当前节点 "休息" 等待
             * parkAndCheckInterrupt 如果确定需要等待,检查是否被中断
             * */

            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true; 
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire(p, node)

次方法会在当前节点 node 获取同步状态失败时执行, 目的是判断线程是否需要阻塞(block), 即是否需要执行后续 park 操作. 整个获取同步状态过程中的信号控制都发生在这个方法中。
节点(线程)的等待状态 waitStatus 有5种:

等待状态waitStatus描述
CANCELLED1由于等待线程超时或者被中断需要取消等待状态, 取消后不可恢复,状态将不会变化
SIGNAL-1后继节点处于等待状态,而前驱节点一旦完成同步操作后会通知后继节点,当前驱节点是此状态时,后继节点可以放心 “休息”
CONDITION-2节点在 Condition 上的等待队列中,不在这里的同步队列,当其他线程对 Condition 调用 signal() 后节点才会转到同步队列
PROPAGATE-3共享式同步状态获取将会无条件传播下去
INITIAL0初始状态

JDK源码的注释已经很详细了:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt()

此方法功能很简单,就是让线程进入 WAITING 状态,返回返回中断状态

park与wait的作用类似,但是对中断状态的处理并不相同。如果当前线程不是中断的状态,park与wait的效果是一样的;如果一个线程是中断的状态,这时执行wait方法会报 java.lang.IllegalMonitorStateException,而执行park时并不会报异常,而是直接返回。

只有以下四种情况中的一种发生时,park 方法才会返回。

  • 与park对应的unpark执行或已经执行时。注意:已经执行是指unpark先执行,然后再执行的park。
  • 线程被中断时。
  • 如果参数中的time不是零,等待了指定的毫秒数时。
  • 发生异常现象时。这些异常事先无法确定。
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); //尝试线程进入 WAITING 状态。
    /*
     * Thread.interrupted() 方法会清空中断标志位. 当park()直接返回说明被中断了,因此清空中断返回 true, 等下次循环时就能 park 了;
     * 如果没有中断,那就 park 成功, 然后返回 false
     */
    return Thread.interrupted();  
}

cancelAcquire() 取消获取

此方法在获取同步状态失败时调用, 作用是将当前节点状态设置为 CANCELED

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    //跳过左右状态为 CANCELLED 的节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    //设置当前节点状态为 CANCELLED
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    //1. 如果当前节点为尾结点,则移除
    //方法是 通过设置当前节点前驱为尾结点 并 设置前驱节点的后继为 null
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.

        int ws;
        //2. 如果不是尾结点 也不是头结点的后继节点 判断当前节点的前驱节点状态
            //如果是 SIGNAL 或 者 设置状态为 SIGNAL 成功,则将前驱节点的后继设为当前节点的后继
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
        //3. 如果当前节点是head的后继节点,则唤醒 当前节点的后继。
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

总结:

  • 当前节点是tail;直接移除,即把前驱节点的后继更新为 null
  • 当前节点不是tail,也不是head的后继节点(即队列的第一个节点,不包括head),两步操作: 1. 设置前驱节点状态为 SIGNAL 2,将前驱节点的后继设为当前节点的后继
  • 当前节点不是tail, 是head的后继节点。unpark后继节点的线程,然后将next指向了自己。

注意上述操作只更新了涉及到的节点的 next ,而没有更新 prev, 这为后面的 unparkSuccessor 方法留了个坑。

参考: 深入理解AbstractQueuedSynchronizer(一)

小结

至此整个 acquire() 方法流程走完了,用一个流程图总结一下:

独占式释放release()

释放同步状态的过程比较简单,首先 tryRelease(int arg) 释放状态, 然后通知后继节点当前同步状态已释放

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    //如果当前节点状态小于0 则将状态置为0.因为当前节点已经完成同步后的操作
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    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;
    }
    if (s != null)
        //唤醒线程
        LockSupport.unpark(s.thread);
}

这里我们看到在找一个有效的等待节点过程是从后向前找的, 结合前面的 cancelAcquire 方法我们就能知道,在前面的 cancelAcquire 方法中更新了 不可用节点(即状态为 CANCELLED)的 next 而没有 更新 prev,因此如果这里从前向后找可用节点有可能陷入死循环。因为在前面的 cancelAcquire 方法中,不可用节点可能把它的 next 指向了自己。

总结

在获取同步状态时, 同步器维护了一个同步队列, 获取状态失败的线程都被加入此队列, 并在队列中进行自旋获取同步状态(前驱节点状态为 SIGNAL, 其他情况节点可能在阻塞)。当前驱节点为 head 头结点并成功获取了同步状态并释放后,节点才会获取到同步状态。这里当头节点释放同步状态时会调用 tryRelease() 然后唤醒后继节点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值