ReentrantLock 源码简单分析

ReentrantLock 源码简单分析

大部分内容来自转载https://www.cnblogs.com/waterystone/p/4920797.html,我只写了stateCondition部分,

  • 作者:水岩

  • 出处:http://www.cnblogs.com/waterystone

  • 本博客中未标明转载的文章归作者水岩和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

目录

ReentrantLock 源码简单分析

节点状态

state

lock

unlock

Condition

await

signalAll

回顾 await 并小结


只简单分析非公平锁的实现,不包含公平锁。

只简单分析主要流程,不涉及中断和异常。

节点状态

这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量 waitStatus 则表示当前 Node 结点的等待状态,共有5种取值 CANCELLED、SIGNAL、CONDITION、PROPAGATE、0

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从条件队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

state

state 是 AbstractQueuedSynchronizer(AQS) 的一个 volatile int 变量,代表着线程同步的资源。state == 0 时代表没有线程竞争资源。ReentrantLock 的资源共享模式是独占模式,当 state1 时,代表线程已经获得锁,其他线程申请锁时会阻塞。ReentrantLock 是可重入锁,当线程已经获得锁时再次申请锁,会增加 state 的值,当线程释放锁时,会减少 state 的值,直到 state 的值为 0 时,其他线程才能申请锁。

lock

线程中调用reentrantLock.lock()方法获取锁。SyncAQS 的抽象子类,FairSyncNonfairSync 这两个类都继承了 Sync 类,分别代表公平锁非公平锁。在lock()的代码里,通过cas判断是否能直接获得锁,如果是重入锁或者其他线程竞争未释放的锁,走acquire(1)方法。

public void lock() {
    sync.lock();
}
// 非公平锁实现
static final class NonfairSync extends Sync {
    //非公平锁的 lock() 方法实际上调用的是这里
    final void lock() {
        //通过cas设置状态,state==0时代表资源没被锁,通过cas修改并设置当前线程所得锁
        if (compareAndSetState(0, 1))
            //设置获得锁的是当前线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //重入锁和其他线程走这里
            acquire(1);
    }
}
//AQS类
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

acquire方法实现在 AQS 类。此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入同步队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:

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

函数流程如下:

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);

  2. addWaiter()将该线程加入同步队列的尾部,并标记为独占模式;

  3. acquireQueued()使线程阻塞在同步队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

以下展示非公平锁的tryAcquire方法实现:

顾名思义,tryAcquire方法的意思就是尝试获取锁。tryAcquire是AQS的抽象方法,在NonfairSync中实现了tryAcquire方法,并在其中调用父类Sync中的nonfairTryAcquire方法。在nonfairTryAcquire方法中首先用 cas 检查 state 是不是为0,如果为 0,说明还没有线程获得锁,可以直接获取锁,如同lock方法。如果state不为0,就检查当前申请锁的线程和独占资源的线程是否为同一个线程,如果是则为重入锁,如果不是则返回false,尝试申请锁失败。

//AQS类抽象方法
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
//非公平Sync类实现tryAcquire方法
static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //获取锁
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //重入锁,检查当前申请锁的线程和独占资源的线程是否为同一个线程
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            //设置状态
            setState(nextc);
            return true;
        }
        //尝试申请锁失败
        return false;
    }
}

接下来看addWaiter方法。此方法用于将当前线程加入到同步队列的队尾,并返回当前线程所在的结点。

private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);
    //尝试快速方式直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //cas检查pred是否是尾节点,如果是则将node插入作为新的尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //上一步失败则通过enq入队。
    enq(node);
    return node;
}

enq方法,此方法用于将node加入队尾。

如果你看过AtomicInteger.getAndIncrement()函数源码,那么相信你一眼便看出这段代码的精华。CAS自旋volatile变量,是一种很经典的用法。还不太了解的,自己去百度一下吧。

private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { //正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t; //返回的是 node 的前节点
            }
        }
    }
}

执行addWaiter方法后,返回封装了当前线程的Node节点,调用acquireQueued方法让Node节点进入等待状态。

OK,通过tryAcquire()addWaiter(),该线程获取资源失败,已经被放入同步队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在同步队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过
        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱,前一个节点,前节点不能为null
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
            if (p == head && tryAcquire(arg)) {
                setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
                p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                failed = false; // 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }
​
            //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。

先看shouldParkAfterFailedAcquire方法:整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的wait状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         * CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

查看parkAndCheckInterrupt方法

如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:

  1. unpark()

  2. interrupt()

注意park(this)ReentrantLock.lock()时,thisReentrantLock对象Condition.await()时,thisCondition对象。

(再说一句,如果线程状态转换不熟,可以参考Thread详解)。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

小结

OK,看了shouldParkAfterFailedAcquire()parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:

  1. 结点进入队尾后,检查状态,找到安全休息点;

  2. 调用park()进入waiting状态,等待unpark()interrupt()唤醒自己;

  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

acquireQueued()分析完之后,我们接下来再回到acquire()!再贴上它的源码吧:

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

再来总结下它的流程吧:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;

  2. 没成功,则addWaiter()将该线程加入同步队列的尾部,并标记为独占模式;

  3. acquireQueued()使线程在同步队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false

  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

由于此函数是重中之重,我再用流程图总结一下:

unlock

线程中调用reentrantLock.unlock()方法释放锁。其中调用sync.release(1)release方法实现在AQS类里。

public void unlock() {
    sync.release(1);
}
//AQS
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒同步队列里的下一个线程
        return true;
    }
    return false;
}

逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!

接下来查看tryRelease方法。此方法尝试去释放指定量的资源。下面是tryRelease()的源码:

//AQS
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
​
//Sync
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);
    }
    //如果state不等于0说明是可重入锁,未完全释放完毕
    setState(c);
    return free;
}

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

接下来看unparkSuccessor方法。此方法用于唤醒同步队列中下一个线程。下面是源码:

//AQS
private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);
​
    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

这个函数并不复杂。一句话概括:用unpark()唤醒同步队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是同步队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了。

小结

release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒同步队列里的其他线程来获取资源。

线程阻塞时,通过LockSupport.park()让线程进入等待状态,通过LockSupport.unpark()唤醒等待的线程。

Condition

await

首先来看await的源码,主要注意几个方法:addConditionWaiterfullyReleaseisOnSyncQueueunlinkCancelledWaiters。不考虑线程中断。

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 唤醒后将尝试获取锁,不在赘述
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters(); //在条件队列中将 waitStatus != Node.CONDITION 的节点清除
    if (interruptMode != 0) //暂时不考虑中断
       reportInterruptAfterWait(interruptMode);
}

addConditionWaiter

addConditionWaiter将当前线程添加到条件队列。首先判断条件队列上是否还有其他节点,如果有就清除条件队列上的那些waitStatus != Node.CONDITION的节点(因为这些节点可能已经添加到同步队列或者已经取消)。接下来以当前线程新建节点,等待状态为CONDITION,之后将此节点加入条件队列末尾,如果当前条件队列还没有节点,新建节点既是头节点也是尾节点。

private Node addConditionWaiter() {
    Node t = lastWaiter;
    //如果条件队列上的节点等待状态已经不是CONDITION,那就会将其从条件队列清除出去
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter; //尾节点可能更新,需要重新设置
    }
    //以当前线程新建节点,等待状态为CONDITION,之后将此节点加入条件队列末尾
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

unlinkCancelledWaiters

如果条件队列上的节点等待状态已经不是CONDITION,那就会将其从条件队列清除出去。代码简单,请读者自行分析。

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        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;
    }
}

fullyRelease

释放资源,不在赘述

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;
    }
}
​
public final boolean release(int arg) {
    //尝试释放锁(资源)
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); //唤醒同步队列上的后继节点
        return true;
    }
    return false;
}

isOnSyncQueue

该方法判断当前节点是不是在条件队列上。在await方法中,会根据节点是不是在同步队列里来判断是否需要退出循环,至于何时将节点放入同步队列,在下面分析signalAll方法时会讲到。

final boolean isOnSyncQueue(Node node) {
    //如果等待状态是CONDITION或者在同步队列上的前置节点为null
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // 如果同步队列上的后置节点不为null
        return true;
    return findNodeFromTail(node);
}
//如果节点位于同步队列上,则通过从尾部向后搜索返回true。仅在isOnSyncQueue需要时调用。
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

signalAll

先上源码。isHeldExclusively简单,主要是需要知道doSignalAll是干什么的。

public final void signalAll() {
    //判断当前线程是否持有锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}
​
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}
//重写
protected final boolean isHeldExclusively() {
    // While we must in general read state before owner,
    // we don't need to do so to check if current thread is owner
    return getExclusiveOwnerThread() == Thread.currentThread();
}

doSignalAll

doSignalAll的源码注释是Removes and transfers all nodes.删除并传输所有节点。删除条件队列上的所有节点并添加到同步队列上。

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first); //添加到同步队列
        first = next;
    } while (first != null);
}

transferForSignal

首先将节点的等待状态从CONDITION设置为0,如果失败就返回(cas,如果预期等待状态不为CONDITION会失败)。之后调用enq方法将节点添加进同步队列并返回节点在同步队列的前置节点。判断前置节点的等待状态,如果是CANCELLED状态会直接唤醒,不通过同步队列唤醒,如果不是CANCELLED状态,会尝试将前置节点的等待状态通过CAS设置成SIGNAL,如果失败也会直接唤醒。(直接唤醒应该属于意外情况,我是菜鸡不能理解直接唤醒的意义,所以我将代码复制过来时也将注释一起复制了)。signalAll是一次性唤醒条件队列上的节点,也就是将多个条件队列的节点转移到同步队列。加入到同步队列的队尾时,只要前置节点不是已经取消调度,就会将前置节点的等待状态设置为SIGNAL,以便在未来唤醒自己。

/**
 * Transfers a node from a condition queue onto sync queue.
 * Returns true if successful.
 * @param node the node
 * @return true if successfully transferred (else the node was
 * cancelled before signal)
 */
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
​
    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

回顾 await 并小结

await方法中,通过判断节点是否在同步队列来选择是否退出循环。调用signalAll之后,会将条件队列的节点加入同步队列,等待同步队列调度唤醒。唤醒时节点已经成为同步队列的头节点,自然在同步队列内,以此跳出循环。

while (!isOnSyncQueue(node)) {
    LockSupport.park(this); //继续等待
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

简单总结Condition的主要流程:

  1. 调用await

  2. 将当前线程封装成节点并添加进条件队列

  3. 释放锁

    注意:释放锁的时候会调用unparkSuccessor方法,唤醒同步队列的第二个节点,此节点或得锁后会将自己变成同步队列的头节点

  4. 进入等待状态

  5. 调用signalAll

  6. 将条件队列的节点转移到同步队列

  7. 等待同步队列调度唤醒

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值