AQS——CLH队列维护方法详解

独占模式下的acquire()方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果tryAcquire()获取不到锁的情况下
        // 将当前线程包装成Node结点在CLH队列中获取到锁后
        // 检测到线程被中断,则调用selfInterrupt
        // 中断处理交给外部调用
        selfInterrupt();
}

解析:

  • acquire()调用自定义的tryAcquire()方法,如果成功了,即获取到了锁,那么不执行后面的相关逻辑;
  • 如果tryAcquire()获取锁失败,那么此时后面会调用addWaiter()方法,将当前线程封装成为一个Node节点加入CLH队列中;
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 新建一个Node结点,设置其中的线程为当前线程,模式为独占
    Node pred = tail;
    // 获得尾结点,如果尾结点不为空,即当前CLH队列不为空
    // 尝试CAS将新建结点插入到队列尾部
    // 如果CLH队列为空或者CAS插入失败,执行下一步逻辑,调用enq()方法
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    // 这里的for循环是一个死循环,保证自旋插入成功
    for (;;) {
        // 同样获得尾结点做非空判断,如果CLH队列为空则进行初始化
        Node t = tail;
        if (t == null) {
            // CAS设置头结点,头结点为一个空姐点,只起到标记作用
            // 因为CLH是一个双向队列,第一个被阻塞线程也需要一个前置结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 初始化完成后,在第二次for循环时进入插入逻辑
            node.prev = t;
            // 直到CAS返回true,才会return,保证了自旋插入的结果,即一定插入成功
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • 插入队列完成后,事实上该结点还未真正结束工作,因为该结点的waitStatu没有进行设置,只是初始化状态下的0,需要进行判断并更新设置才算真正完成入队操作,即调用acquireQueued()方法:
final boolean acquireQueued(final Node node, int arg) {
    // 当前线程是否获得了锁,只有当获得了锁的情况下,该标志位会被设置为false
    boolean failed = true;
    try {
        // 当前线程是否被中断
        boolean interrupted = false;
        // for循环自旋实现FIFO
        for (;;) {
            final Node p = node.predecessor();
            // 更新waitStatus之前最后一次尝试获取锁
            // 如果获取锁成功则更新CLH队列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; 
                // 设置为null协助GC
                failed = false;
                // 返回是否被中断
                return interrupted;
            }
            // shouldParkAfterFailedAcquire:更新当前线程的waitStatus,完成真正入队
            // parkAndCheckInterrupt:阻塞线程并检查是否被中断
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果线程异常退出,且当前线程没有获取到锁
        // 使用try-fianlly结构保证将线程waitStatus状态设置为Cancelled
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 如果前置结点的状态为SIGNAL(-1),直接返回true
    // 表示已经完成入队操作,直接阻塞当前线程
    if (ws == Node.SIGNAL)
        return true;
    // 如果前置结点的状态大于0,即为CANCELLED,放弃争夺锁
    // 更新CLH队列,将当前结点的前置结点设置为正常结点
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果前置结点的状态为0(原始的尾结点状态应该为0)
        // 那么将其状态设置为SIGNAL,在下一次for循环中调用时
        // 便可以进入第一个if逻辑中,返回true
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    // 阻塞当前线程
    LockSupport.park(this);
    // 返回当前线程是否被中断,同时清除中断标志位
    // 将中断处理隔绝在AQS获取独占锁的过程外
    return Thread.interrupted();
}

独占模式下的release()方法:

public final boolean release(int arg) {
    // 如果释放资源成功,那么唤醒下一个结点的线程
    if (tryRelease(arg)) {
        // 获取head结点
        Node h = head;
        // head结点不为空且head结点的waitStatus则唤醒下一个结点中的线程
        // CLH队列初始化后的head结点代表正在运行的线程(结点中的Thread为空但waitStatus为-1)
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {

    // CLH头结点的状态
    int ws = node.waitStatus;
    // 置零当前线程所在的结点状态,允许失败
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 找到下一个需要唤醒的结点
    Node s = node.next;
	// 如果next结点的状态不符合要求,那么从CLH队列的
    // 尾端开始往前遍历进行唤醒
    // 这里只是唤醒线程而不是使其获得锁
    // 真正获得锁的过程还是在acquire()方法中
    // 只有head结点的下一个结点才会获取到资源,保证FIFO
    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);
}

共享模式下的AQS将状态state的初始值设置为N,表示空闲。每次争取锁的时候就利用CAS操作让state减1,直到减到0表示非空闲为止,且每次争取到锁的时候通过CAS设置新的CLH队列的头部,保证FIFO。释放同步状态时,需要进行CAS操作,因为共享模式下有多个线程可以获取到同步状态,不想独占模式下只有一个线程对state进行修改。

参考文章

掘金——《提升能力,涨薪可待》-Java并发之AQS全面详解
掘金——Java并发之AQS详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQS(AbstractQueuedSynchronizer)是Java中实现同步器的框架,它提供了一种基于FIFO队列的阻塞和唤醒机制。AQS的阻塞队列原理是通过CLH(Craig, Landin, and Hagersten)队列来实现的。 CLH队列是一种虚拟的双向链表,它仅存在节点之间的关联关系,而不存在队列的实例。每个请求共享资源的线程都会被封装成一个CLH队列的节点(Node)。当线程请求共享资源时,它会被添加到CLH队列的尾部,并进入阻塞状态。 当共享资源被占用时,其他线程请求该资源的线程会被放入CLH队列的末尾,即排队等待。这种排队等待的方式可以保证请求资源的线程按照FIFO的顺序获得资源,避免了饥饿现象。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。 需要注意的是,AQS的同步队列(Sync queue)是一个双向链表,包括头节点(head)和尾节点(tail),用于后续的调度。而条件队列(Condition queue)是一个单向链表,只有在使用Condition时才会存在,并且可能会有多个条件队列。 总结一下,AQS实现阻塞队列的原理是通过CLH队列来实现的,当共享资源被占用时,请求资源的线程会被添加到CLH队列中排队等待。当资源释放后,AQS会自动唤醒队列中的下一个线程,使其获得资源并继续执行。同步队列用于后续的调度,而条件队列只在使用Condition时才会存在

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值