并发入门组件AQS源码解析(未完善)

news2024/11/25 15:30:29

必要掌握技术

阻塞,唤醒与中断

阻塞与唤醒

LockSupport的park使用的是Parker->park()
synchronized的wait,synchronized的monitorenter使用的是ParkEvent->park(),
而LockSupport的unpark,Parker->unpark()
synchronized的signal,synchronized的monitorexit都是使用的ParkEvent->unpark()。
Parker和ParkEvent是两个极为相似的类,都使用pthread_cond_wait这个方法完成阻塞,pthread_cond_signal这个方法来唤醒。
其中monitorexit并没有看到过源码解析,但是monitorenter在阻塞时用的是park,那么monitorexit基本也会是unpark。

中断

中断和唤醒的两大关系

如果中断,那么就无法阻塞(或者会被从阻塞中唤醒),这和各自的实现有关,如Parker->park()会直接返回而避免进入阻塞。
此外,如果使用的是Parker->park()(也就是LockSupport.park(),须知AQS是使用此方法完成阻塞,所以很多并发组件都是这类),如果曾经中断过,就算清除了中断位,也会造成下一次park的豁免。
这是因为interrupt调用了Parker->unpark(),将许可证赋值为1了。
在这里插入图片描述

park先判断许可证是否为1,如果有许可证直接许可证归0然后返回,再判断是否中断,如果中断直接返回,如果都不满足,才会继续执行阻塞逻辑,被唤醒后,会将许可证归0,结束方法。
在这里插入图片描述

在这里插入图片描述

unpark会直接将许可证赋值为1,然后尝试唤醒阻塞线程。
更多源码阅读可以观看参考资料

对于中断报错的思考

为什么synchronized监视器不存在异常报错,而synchronized的wait()存在,这是因为JVM在阻塞唤醒后添加了对中断位的判断,如果判断通过,会爆出一次,拥有这个异常的方法如wait/sleep/join等。

对于中断无法对synchronized监视器锁起作用的思考

interrupt虽然可以唤醒阻塞的线程,但是完成park阻塞的块被死循环了,就算唤醒,也会重新回到park,而中断并没有被设置为break的条件。

参考资料

Parker和ParkEvent
https://blog.csdn.net/qq_31865983/article/details/105184585
monitorenter源码解析
https://blog.csdn.net/u013643074/article/details/125596328
park源码解析
https://blog.csdn.net/fengyuyeguirenenen/article/details/122997847
https://blog.csdn.net/weixin_43406582/article/details/119540260(内含interrupt源码)
wait源码解析
https://blog.csdn.net/qq886666/article/details/124101527
关于中断异常的解析
https://www.cnblogs.com/niuyourou/p/12392942.html

阻塞队列和等待队列

在这里插入图片描述

阻塞队列和同步队列是JUC锁的两个基本机制,对于synchronized而言,获取不到监视器锁的线程会进入阻塞队列,对于AQS来说,tryAcquire()竞争失败的节点也会进入阻塞队列。synchronized的wait()会使获取监视器锁的节点放弃锁,而进入唯一的等待队列,AQS中condition的await()会使lock锁的节点放弃锁,进入对应condition的等待队列。当他们被唤醒时,就会执行wait()/await()中的后续逻辑,重新获取锁(依然需要和其他线程竞争),获取到之后便会从wait()/await()返回,执行原先调用wait()/await()点的后续逻辑。

AQS中的Condition

await()

首先会通过addConditionWaiter()向等待队列添加节点,然后尝试释放锁,持续陷入阻塞,直到被中断或者判断在阻塞队列了。

public final void await() throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果不在阻塞队列
    while (!isOnSyncQueue(node)) {
    	//阻塞
        LockSupport.park(this);
        //中断的判断
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //进入阻塞方法(acquireQueued),这个方法在获取独占锁时有详细说明,这个方法的返回值是中断位,如果被中断了并且没有发生异常,那么interruptMode = REINTERRUPT,后期就按中断处理了
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // 如果后面添加了其它节点,进行清除,在丢出取消节点有详细描述
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

等待队列添加节点

如果当前尾节点不是Condition状态,会使用unlinkCancelledWaiters()从头到尾清除一遍。然后插入一个状态为CONDITION的尾节点。
为什么阻塞队列的遍历往往是从尾到头,而等待队列可以从头到尾,这是因为等待队列的操作已经默认是在锁中进行了,没有阻塞队列激烈的头节点判断机制。

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

释放锁

再次提醒,释放锁是await()的一部分,用于从阻塞队列取出这个节点。
释放锁的逻辑相对简单,实际上就是调用了release(),对于release()的解析可以看释放独占锁章节。

final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
        //如果失败,会直接返回取消状态
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

值得注意的是,释放锁的过程中,重写的tryRelease()往往会尝试判断是否拥有独占锁,如果没有,那么就会报错。也就是说,执行await()必定是线程安全的语义是必须自己重写完成的。以下是ReentrantLock的重写示范。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
	/**
    	如果当前线程没有拥有锁,就会报错提示
	**/
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

如果没有拥有锁而报错,线程会进入fullyRelease(Node node)的finally块,将节点置为取消状态,然后在signal执行transferForSignal(Node node)时由于状态不是等待而被直接剔除(见signal()的正式唤醒逻辑)。

是否在阻塞队列的判断

这里有一点容易混淆,prev和next并不是等待队列的指针,而是阻塞队列的,等待队列是一个以nextWaiter为指针的单向队列。
如果node状态为CONDITION(发生了await()),或者前继节点为空(阻塞队列节点必有前指针),会判断不在阻塞队列。如果有后继节点,那么必定在阻塞队列。如果都不符合,可能暂时位于尾部,那会从尾到头尝试从阻塞队列找到该节点,如果找不到,说明还在等待队列,返回false。

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;
    /*
     * node.prev can be non-null, but not yet on queue because
     * the CAS to place it on queue can fail. So we have to
     * traverse from tail to make sure it actually made it.  It
     * will always be near the tail in calls to this method, and
     * unless the CAS failed (which is unlikely), it will be
     * there, so we hardly ever traverse much.
     */
    return findNodeFromTail(node);
}

将取消节点丢出

private void unlinkCancelledWaiters() {
		//取出头节点
        Node t = firstWaiter;
        Node trail = null;
        while (t != null) {
            Node next = t.nextWaiter;
            //如果当前节点不是等待状态,就将其断开连接,然后让trail连接上下一个。可以将trail理解为上一个状态为CONDITION的节点。
            if (t.waitStatus != Node.CONDITION) {
                t.nextWaiter = null;
                if (trail == null)
                    firstWaiter = next;
                else
                    trail.nextWaiter = next;
                if (next == null)
                    lastWaiter = trail;
            }
            else
                trail = t;
            t = next;
        }
}

signal()

如果没有拥有独占锁,报错。如果等待队列有节点进入唤醒。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

正式唤醒逻辑

将首节点向后移一个。
如果没有下一个了,就把尾指针也清空。
然后清空不需要的节点。
直到唤醒一个正在等待的节点或遍历完为止。

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

尝试将状态从CONDITION换成正在运行,如果成功了,就会将其入队(关于enq()逻辑可以看追加尾节点章节),p为node在阻塞队列中的前继节点。如果前继节点是取消状态,或者单次尝试把前继节点状态换成阻塞失败,那么会唤醒当前节点的线程,这时节点的线程会继续执行wait()中的逻辑(见await()章节),它会进行前继节点signal状态更新然后完成自我阻塞(调用的acquiredQueue()逻辑见自我阻塞章节)。

final boolean transferForSignal(Node node) {
    /*
    * If cannot change waitStatus, the node has been cancelled.
    */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    	return false;

    /*
    * yingyufanyi
    */
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

AQS的意义

AQS的作用是减少锁争抢带来的消耗,它阻塞了除获取到锁的头节点外的其它节点的线程,并将阻塞线程以队列的形式保存起来。当头节点的线程释放锁后,会唤醒自己的后继节点,唤醒的节点会将自己设为新的头节点。在AQS抽象类中实现了从队列增删,中断,时间判断的功能。而获取锁和释放锁的逻辑待实现,因为研发者希望让它尽可能地作为更多并发工具的组件。

重写方法

AQS中有五个待重写方法
以下两个用来尝试获取和释放独占锁
tryAcquire(int arg)
tryRelease(int arg)
以下两个用来尝试获取和释放共享锁
tryAcquireShared(int arg)
tryReleaseShared(int arg)
最后一个用来判断当前线程是否占有着独占锁
isHeldExclusively(),在AQS的父类中,有着setExclusiveOwnerThread()和getExclusiveOwnerThread()来判断线程的独占关系

3种获取独占锁的方式

acquire(int arg)

简要介绍

如果获取锁失败,那么会加到尾节点,然后自我阻塞,通过if会加一个中断位。这是因为在acquiredQueue中,会清除中断,避免失败阻塞。

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

尝试获取锁

这个方法由继承子类重写,体现了一种多态性,需要明白的是获取锁不仅可以在里面加入一些公平或非公平的争抢,还可以加入多次CAS尝试。所以一次尝试tryAcquire并非一次尝试获取锁,我们可以根据自己业务需求,额外增加尝试次数而减少阻塞带来的上下文切换消耗。

追加尾节点

addWaiter是用于追加到尾节点的,mode只有Node.EXCLUSIVE和Node.SHARED这两种静态成员常量。if判断中,是尝试获取一次尾节点,如果可以成功的话,说明没有竞争,直接返回。
CAS前优先完成prev指针指向,这样,在CAS成功的那一刻,队列就已经连通了,这也和队列遍历顺序有关(队列是从尾到头遍历的),也和AQS经常使用前继节点作为判断条件有关。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

事实上,在AQS构造函数是空的,并没有完成对于AQS队列的初始化,只有在第一次被获取锁时才会开始判断,如果尾节点是null,那么会尝试初始化一个节点作为首尾节点。
之后会一直进入else,陷入尾节点的尝试加入。这和addWaiter中if逻辑一致,这样写的好处是,在低并发的时候可以无视enq方法,给人一种简化的感觉,如注释所言,一条快捷的路径。

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;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

自我阻塞

for循环就是阻塞的逻辑。如果当前节点前继节点为头节点,就会获得一次尝试获取的机会,如果成功获取,那么会将自己设为头节点,然后返回中断位。
如果parkAndCheckInterrupt()为true,也就意味着之前是被中断的,那么会把中断位记录下来,等到返回acquire()层会调用selfInterrupt()实现自我中断。
如果中间出现了错误,那么会直接进入failed,将节点置为取消状态。cancelAcquire源码解析放在删除节点中。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

如果前一个节点处于等待唤醒状态,那么返回true。
如果前一个节点被取消了(只有这个情况waitStatus>0),那么把它给删除,直到遇到第一个不被取消的节点为止,由于头节点的存在,所以并不会出现空指针。最后把有效的前继节点指向自己,中间的就会因为不可达而被回收。否则把前继改为等待唤醒状态。
node.prev = pred = pred.prev;这句有点绕口,虽然逻辑没错,但一般应该是pred = pred.prev; node.prev =pred ;这是因为取消时候这两种情况都返回了false,这是因为出现这两种情况可能会让节点拥有获得锁的机会,因为它上一个节点可能已经是头节点了。

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;
}

阻塞当前线程并返回是否清空中断位

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

acquireInterruptibly(int arg)

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

很简单,首先加入队列尾,然后判断是否轮到自己,在第二个if中将自己前面节点设为signal,唯一不同的点在于通过第二个if后并不是保存中断,而是直接抛出异常。

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

tryAcquireNanos(int arg, long nanosTimeout)

这部分重复代码依旧很多,额外的逻辑如下:
deadline是预计终止时间,在for循环中,每次nanosTimeout都会更新剩余时间,如果小于1秒,那么不会进入阻塞,这是因为线程阻塞和唤醒也需要时间,1秒钟内时让它循环判断,可以提高响应速度,给人更准时的感觉。另外,此方法也响应中断,中断发生后会进入finally逻辑删除当前节点。

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放独占锁

tryRelease(arg)需要自己实现,需要注意的是由于重入问题,即使成功执行一次,也只是减少一次重入,而继续返回false。
head为null说明队列没有完成初始化,head状态为0说明队列只完成了初始化,或者曾经被唤醒过。
如果成功释放了锁,那么会唤醒下一个节点。

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) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    //首先完成节点状态归0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    //如果下一个被取消了,那么从尾到头寻找第一个待唤醒节点
    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);
}

获取共享锁

尝试获取一次共享锁,如果失败,加入队列,自我阻塞。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

尝试获取共享锁

如果没人拥有写锁或者自己拥有写锁,就可以在其中持续尝试获取读锁。可以分为公平和非公平,公平情况下,可以使用hasQueuedPredecessors()阻止后继节点提前获取读锁(即使不用也不会造成共享语义的错误)。
是否是读锁可以通过state或者waitStatus来判断。
在ReentrantReadWriteLock中,就以state的前16位记录目前持有读锁的线程的数量,后16位记录目前持有写锁的线程的数量。

doAcquire执行

尝试队尾加入节点(在acquire(int arg)的追加尾节点有相应逻辑),然后自我阻塞,每次被唤醒后会尝试获取共享锁,获取成功,将自己设为头节点。
在源码中Node.SHARED对应的是共享节点,

private void doAcquireShared(int arg) {
	//addWaiter方法在acquire(int arg)的追加尾节点有相应逻辑
    final Node node = addWaiter(Node.SHARED);
    //老套路了,成功执行改为false,不成功finally用来将节点设为取消状态
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这里将当前点设置成新的头节点,但是并没有唤醒后继节点,而是放在doReleaseShared中唤醒。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    /*
     * Try to signal next queued node if:
     *   Propagation was indicated by caller,
     *     or was recorded (as h.waitStatus either before
     *     or after setHead) by a previous operation
     *     (note: this uses sign-check of waitStatus because
     *      PROPAGATE status may transition to SIGNAL.)
     * and
     *   The next node is waiting in shared mode,
     *     or we don't know, because it appears null
     *
     * The conservatism in both of these checks may cause
     * unnecessary wake-ups, but only when there are multiple
     * racing acquires/releases, so most need signals now or soon
     * anyway.
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

如果本身就是活跃的,那么说明它是其中共享的一个读节点,目前作为队列最前的头节点,需要设置状态为共享,这样其它进入的线程才能正确判断读状态。如果都执行完,就退出,(h==head 不满足说明其他线程修改了头节点,那需要从头进行head的判断)。

private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases.  This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
    for (;;) {
        Node h = head;
        //h!=null和h!=tail保证了阻塞队列至少有一个Node
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //如果这个节点被设置成SIGNAL,说明需要释放后续节点
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            //如果不为signal,说明后面还有节点保持读状态,将节点改为PROPAGETE
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

删除节点

首先判断节点是否已经被删除了,因为删除本身并没有加锁。
取消的逻辑在自我阻塞时也有出现过,那多线程取消是否会出现并发数据安全问题,并不会。因为删除过程并没有动任何的其他点的prev指针,它们都可以共同向前遍历。
如果自己是尾节点,直接去掉就好。
否则,会判断自己是否是头节点,或者头节点的竞争者,如果是其中某种情况,会唤醒后继者,否则把前继节点指向后继节点。

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

    node.thread = null;

    // Skip cancelled predecessors
    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.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    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;
        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 {
            unparkSuccessor(node);
        }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13809.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

员工离职后,账号权限怎么自动化回收?

最近一则离职员工报复前东家的新闻引人注目。起因是该员工被公司辞退后怀恨在心&#xff0c;于是用自己的笔记本电脑入侵了前公司的服务器&#xff0c;进入了该公司的法国站、英国站、德国站三个亚马逊店铺&#xff0c;进行了大幅调低商品价格、主动向客户发起退款、调高广告预…

1525_AURIX TC275 BootROM上

全部学习汇总&#xff1a; GitHub - GreyZhang/g_TC275: happy hacking for TC275! 这一次看一个全新的章节&#xff0c;BootROM&#xff0c;这是我之前只听过但是没有接触过的一个功能。 1. BootROM包含的三个主要的功能&#xff1a;启动软件、引导加载程序、测试固件。 2. 启…

UI设计都有哪些设计原则,分享三个给你

是什么使一个好UI设计容易阅读&#xff1f;是什么让用户轻松浏览&#xff1f;设计师如何创造一个闪亮的UI&#xff1f;任何软件产品的关键部分都是用户界面。 ​好的UI设计&#xff0c;用户甚至会忽略它。如果做得不好&#xff0c;就会成为用户使用产品的绊脚石。为了更有效地设…

数字化车间认定条件

一、申报数字化车间的奖励&#xff1a; 聊城市为了支持企业开展智能制造。对新获认定的国家级智能制造示范工厂、智能制造优秀场景&#xff0c;分别给予最高100万元、50万元一次性奖励&#xff1b;对新获认定的省级智能制造系统解决方案供应商、智能制造标杆企业、智能工厂、数…

因误删文件导致CentOS7开机卡死无法进入图形登录界面

目录 1、背景 2、解决步骤 1、背景 这几天在清理电脑&#xff0c;需要删除虚拟机&#xff0c;为此写下了Linux系统下卸载VMware Workstation软件_nanke_yh的博客-CSDN博客&#xff0c;但是同时怕有残留&#xff0c;自己全局搜索了vm&#xff0c;删除了部分带有vm的文件。删除…

【GridMask】《GridMask Data Augmentation》

arXiv-2020 文章目录1 Background and Motivation2 Related Work3 Advantages / Contributions4 GridMask5 Experiments5.1 Image Classification5.2 Object Detection on COCO Dataset5.3 Semantic Segmentation on Cityscapes5.4 Expand Grid as Regularization6 Conclusion&…

MongoDB之完整入门知识(集合操作、文档基本CRUD操作、文档分页查询、索引等相关命令)

MongoDB完整入门知识一、相关概念1、简介2、体系结构3、安装网址二、MongoDB基本常用命令1、Shell连接&#xff08;mongo命令&#xff09;2、选择和创建数据库2.1 选择和创建数据库的语法格式&#xff08;如果数据库不存在&#xff0c;则自动创建&#xff09;2.2 查看有权限查看…

SpringBoot与Loki的那些事

因为网上好多都没有通过Loki的API自己实现对日志监控系统&#xff0c;所以我就下定决心自己出一版关于loki与springboot的博文供大家参考&#xff0c;这个可以说是比较实用&#xff0c;很适合中小型企业。因此我酝酿了挺久了&#xff0c;对于loki的研究也比较久&#xff0c;希望…

论文精读《OFT: Orthographic Feature Transform for Monocular 3D Object Detection》

OFT: Orthographic Feature Transform for Monocular 3D Object Detection 文章目录OFT: Orthographic Feature Transform for Monocular 3D Object Detection论文精读摘要&#xff08;Abstract&#xff09;1. 介绍&#xff08;Introduction&#xff09;2. 相关工作&#xff08…

给开源项目做一个漂亮简洁的版本迭代更新图,生成固定链接复制到介绍中、公众号菜单链接中、博客中等

背景 开源项目的版本迭代与更新经常需要更新迭代文档&#xff0c;但是readme.md没有比较美观一点的效果&#xff0c;所以文本分享一种第三方的方式&#xff1a;用TexSpire的免费在线文档分享功能&#xff0c;手机、PC、Pad都可以适配。 效果预览 使用 第一步&#xff1a;创…

浅谈 async/await 和生成器

浅谈 async/await async/await 是ES8规范新增的&#xff0c;使得以同步方式写的代码异步运行不再是白日梦&#xff0c;进一步让代码逻辑更加清晰。 为什么新增 async/await 下面有这样一个需求&#xff1a;有两个请求&#xff0c;请求 1 的结果是请求 2 的参数&#xff0c;所…

机器学习6——EM算法与高斯混合模型GMM

前置内容 Jensen不等式 高斯混合模型 多元高斯模型 拉格朗日乘子法 主要内容 EM算法&#xff08;Expectation-Maximization&#xff09;&#xff0c;期望-最大化。 用于保证收敛到MLE&#xff08;最大似然估计&#xff09;。主要用于求解包含隐变量的混合模型&#xff0c;主要…

R生成三线表

R生成三线表table1包测试数据生成制作三线表Tableone包加载包 探索数据类型数据整理分类构建Table函数Tableone函数细节主要基于table1 或tableone 包table1包 测试数据生成 datadata.frame(性别sample(c("男","女"), 1000,replaceT),年龄round(rnorm(10…

2021年认证杯SPSSPRO杯数学建模A题(第一阶段)医学图像的配准全过程文档及程序

2021年认证杯SPSSPRO杯数学建模 A题 医学图像的配准 原题再现&#xff1a; 图像的配准是图像处理领域中的一个典型问题和技术难点&#xff0c;其目的在于比较或融合同一对象在不同条件下获取的图像。例如为了更好地综合多种信息来辨识不同组织或病变&#xff0c;医生可能使用…

5年自动化测试,终于进字节跳动了,年薪30w其实也并非触不可及

一些碎碎念 什么都做了&#xff0c;和什么都没做其实是一样的&#xff0c;走出“瞎忙活”的安乐窝&#xff0c;才是避开弯路的最佳路径。希望我的经历能帮助到有需要的朋友。 在测试行业已经混了5个年头了&#xff0c;以前经常听到开发对我说&#xff0c;天天的点点点有意思没…

java计算机毕业设计springboot+vue+elementUI永加乡精准扶贫信息管理系统

项目介绍 系统设计的主要意义在于&#xff0c;一方面&#xff0c;对于网站来讲&#xff0c;系统上线后可以带来很大的便利性&#xff0c;精准扶贫网站管理属于非常细致的管理模式&#xff0c;要求数据量大&#xff0c;计算机管理可以提高精确性&#xff0c;更为便利的就是信息…

NF-κB 信号通路调节细胞因子转录

NF-κB 大家族哺乳动物 NF-κB 家族由五种成员组成&#xff1a;RelA/p65、c-Rel、RelB、p50 (NF-κB1) 和 p52 (NF-κB2)&#xff0c;它们可以形成各种异源二聚体或者同源二聚体 (如常见 p50/RelA 异源二聚体)&#xff0c;并通过与启动子的 κB 位点结合来激活大量基因。所有 N…

Mysql常用函数

Mysql常用函数 字段拼接(concat) CONCAT() 函数用于将多个字符串连接成一个字符串 格式&#xff1a; select CONCAT(str1,str2,…) from table_name; #查询商品表&#xff0c;返回一列&#xff1a;商品名称&#xff08;价格&#xff09;。 SELECT concat(prod_name,(,prod…

【论文阅读】Weakly Supervised Semantic Segmentation using Out-of-Distribution Data

一篇弱监督分割领域的论文&#xff0c;发表在CVPR2022上&#xff1a; 论文标题&#xff1a; Weakly Supervised Semantic Segmentation using Out-of-Distribution Data 作者信息&#xff1a; 代码地址&#xff1a; https://github.com/naver-ai/w-ood Abstract 作者认为…

专精特新小巨人的申报条件

专精特新企业分为市级专精特新、省级专精特新和国家级专精特新。 在2018年&#xff0c;开展了国家第一批专精特新“小巨人” 企业申报工作。为了引导中小企业积极走“专精特新”发展之路&#xff0c;加快新旧动能转 换步伐&#xff0c;提升自主创新能力、加快转型升级&#xf…