Lock Condition源码分析

本文详细分析了Java并发库J.U.C中的Condition概念及其核心方法,包括await、signal和signalAll的实现原理。Condition允许在持有锁的基础上进行条件阻塞和唤醒,其内部维护了一个独立的等待队列,通过AQS进行线程的阻塞和唤醒管理。文章通过实例阐述了Condition如何在多线程环境下实现线程的有条件等待和通知。
摘要由CSDN通过智能技术生成

目录

Condition的概念

conditon.await()和condition.signal()

大体实现流程

Condition的数据结构

线程何时阻塞和释放

await方法

signal和signalAll方法


Condition的概念

JUC提供了Lock可以方便的进行锁操作,但是有时候我们也需要对线程进行条件性的阻塞和唤醒,这时我们就需要condition条件变量,它就像是在线程上加了多个开关,可以方便的对持有锁的线程进行阻塞和唤醒。

Condition主要是为了在J.U.C框架中提供和Java传统的监视器风格的wait,notify和notifyAll方法类似的功能。

在JUC锁机制(Lock)学习笔记中,我们了解到AQS有一个队列,同样Condition也有一个等待队列,两者是相对独立的队列,因此一个Lock可以有多个Condition,Lock(AQS)的队列主要是阻塞线程的,而Condition的队列也是阻塞线程,但是它是有阻塞和通知解除阻塞的功能 
Condition阻塞时会释放Lock的锁,阻塞流程请看下面的Condition的await()方法。

conditon.await()和condition.signal()


ReetnrantLock lock
condition=lock.newCondition


condition.await(每个condtion内部都有自己的一个队列):

  1. 会释放lock锁
  2. 将当前线程从AQS队列移除,添加到条件队列contion尾部
  3. 调用Locksupport.park()进行阻塞(阻塞在await方法内部)

condition.signal()

  1. 唤醒condition队列的头结点 添加到AQS队列,
  2. 解除阻塞Locksupport.unpark()
  3. 此时执行await方法里的自旋获取锁的方法,获取成功后,从conditon队列移除,await执行完毕。原线程接着从await的下一行代码开始执行,实现了唤醒。

大体实现流程

AQS等待队列与Condition队列是两个相互独立的队列 
await()就是在当前线程持有锁的基础上释放锁资源,并新建Condition节点加入到Condition的队列尾部,阻塞当前线程 
signal()就是将Condition的头节点移动到AQS等待节点尾部,让其等待再次获取锁

以下是AQS队列和Condition队列的出入结点的示意图,可以通过这几张图看出线程结点在两个队列中的出入关系和条件。

I.初始化状态:AQS等待队列有3个Node,Condition队列有1个Node(也有可能1个都没有)

II.节点1执行Condition.await() 
1.将head后移 
2.释放节点1的锁并从AQS等待队列中移除 
3.将节点1加入到Condition的等待队列中 
4.更新lastWaiter为节点1

III.节点2执行signal()操作 
5.将firstWaiter后移 
6.将节点4移出Condition队列 
7.将节点4加入到AQS的等待队列中去 
8.更新AQS的等待队列的tail

Condition的数据结构

我们知道一个Condition可以在多个地方被await(),那么就需要一个FIFO的结构将这些Condition串联起来,然后根据需要唤醒一个或者多个(通常是所有)。所以在Condition内部就需要一个FIFO的队列。 
private transient Node firstWaiter; 
private transient Node lastWaiter; 
上面的两个节点就是描述一个FIFO的队列。我们再结合前面提到的节点(Node)数据结构。我们就发现Node.nextWaiter就派上用场了!nextWaiter就是将一系列的Condition.await*串联起来组成一个FIFO的队列。

线程何时阻塞和释放

阻塞:await()方法中,在线程释放锁资源之后,如果节点不在AQS等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal()后,节点会从condition队列移动到AQS等待队列,则进入正常锁的获取流程

await方法

ReentrantLock是独占锁,一个线程拿到锁后如果不释放,那么另外一个线程肯定是拿不到锁,所以在lock.lock()和lock.unlock()之间可能有一次释放锁的操作(同样也必然还有一次获取锁的操作)。在进入lock.lock()后唯一可能释放锁的操作就是await()了。也就是说await()操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!

public final void await() throws InterruptedException {
    // 1.如果当前线程被中断,则抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 2.将节点加入到Condition队列中去,这里如果lastWaiter是cancel状态,那么会把它踢出Condition队列。
    Node node = addConditionWaiter();
    // 3.调用tryRelease,释放当前线程的锁,释放锁后会unparkAQS队列中下一个节点并作为新的头,本节点从AQS出队
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    // 4.为什么会有在AQS的等待队列的判断?
    // 解答:signal操作会将Node从Condition队列中拿出并且放入到等待队列中去,在不在AQS等待队列就看signal是否执行了
    // 如果不在AQS等待队列中,就park当前线程,如果在,就退出循环,这个时候如果被中断,那么就退出循环
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了4中的while循环
    // 自旋等待尝试再次获取锁,调用acquireQueued方法
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();//这里是从condition队列移除此节点
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

整个await的过程如下: 
  1.将当前线程加入Condition锁队列。特别说明的是,这里不同于AQS的队列,这里进入的是Condition的FIFO队列。进行2。 
  2.释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。进行3。 
  3.自旋(while)挂起,直到被唤醒或者超时或者CACELLED等。进行4。 
  4.获取锁(acquireQueued)。并将自己从Condition的FIFO队列中释放,表明自己不再需要锁(我已经拿到锁了)。

可以看到,这个await的操作过程和Object.wait()方法是一样,只不过await()采用了Condition队列的方式实现了Object.wait()的功能。

signal和signalAll方法

await*()清楚了,现在再来看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要将Condition.await*()中FIFO队列中第一个Node唤醒(或者全部Node)唤醒。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

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

这里先判断当前线程是否持有锁,如果没有持有,则抛出异常,然后判断整个condition队列是否为空,不为空则调用doSignal方法来唤醒线程,看看doSignal方法都干了一些什么:

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
  • 码很容易看出来,signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程。当然了遇到CANCELLED线程就需要将其从FIFO队列中剔除。
final boolean transferForSignal(Node node) {
    /*
     * 设置node的waitStatus:Condition->0
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * 将节点加入到AQS的等待队列,让节点继续获取锁
     * 设置前置节点状态为SIGNAL
     */
    Node p = enq(node);
    int c = p.waitStatus;
    if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

上面就是唤醒一个await*()线程的过程,根据前面的介绍,如果要unpark线程,并使线程拿到锁,那么就需要线程节点进入AQS的队列。所以可以看到在LockSupport.unpark之前调用了enq(node)操作,将当前节点加入到AQS队列。

signalAll和signal方法类似,主要的不同在于它不是调用doSignal方法,而是调用doSignalAll方法:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter  = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
  •  

这个方法就相当于把Condition队列中的所有Node全部取出插入到等待队列中去。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值