目录

Condition的概念

大体实现流程

  I.初始化状态

  II.await()*作

  III.signal()*作

3个主要方法

  Condition的数据结构

  线程何时阻塞和释放

  await()方法

  signal()和signalAll()方法

Condition示例:生产者和消费者

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

Condition的概念

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

 

JDK的官方解释如下:

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属*是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition实质上是被绑定到一个锁上。

 

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

3个主要方法

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()*作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!

 

 Java Code 

  

public final void await() throws InterruptedException {

    // 1.如果当前线程被中断,则抛出中断异常

    if (Thread.interrupted())

        throw new InterruptedException();

    // 2.将节点加入到Condition队列中去,这里如果lastWaiter是cancel状态,那么会把它踢出Condition队列。

    Node node = addConditionWaiter();

    // 3.调用tryRelease,释放当前线程的锁

    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();

    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)。

Java Code 

  

public final void signal() {

    if (!isHeldExclusively())

        throw new IllegalMonitorStateException();

    Node first = firstWaiter;

    if (first != null)

        doSignal(first);

}

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

 Java Code 

  

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队列中剔除。

 

 Java Code 

  

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方法:

 Java Code 

  

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全部取出插入到等待队列中去。

Condition应用示例:生产者和消费者

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。在最后我们来看一个应用示例

 Java Code 

  

/**

 * 生产者、消费者示例

 */

public class ConditionTest {

    private int storage;

    private int putCounter;

    private int getCounter;

    private Lock lock = new ReentrantLock();

    private Condition putCondition = lock.newCondition();

    private Condition getCondition = lock.newCondition();

public void put() throws InterruptedException {

        try {

            lock.lock();

            if (storage > 0) {

                putCondition.await();

            }

            storage++;

            System.out.println("put => " + ++putCounter );

            getCondition.signal();

        } finally {

            lock.unlock();

        }

    }

public void get() throws InterruptedException {

        try {

            lock.lock();

            lock.lock();

            if (storage <= 0) {

                getCondition.await();

            }

            storage--;

            System.out.println("get  => " + ++getCounter);

            putCondition.signal();

        } finally {

            lock.unlock();

            lock.unlock();

        }

    }

public class PutThread extends Thread {

        @Override

        public void run() {

            for (int i = 0; i < 100; i++) {

                try {

                    put();

                } catch (InterruptedException e) {

                }

            }

        }

    }

public class GetThread extends Thread {

        @Override

        public void run() {

            for (int i = 0; i < 100; i++) {

                try {

                    get();

                } catch (InterruptedException e) {

                }

            }

        }

    }

public static void main(String[] args) {

        final ConditionTest test = new ConditionTest();

        Thread put = test.new PutThread();

        Thread get = test.new GetThread();

        put.start();

        get.start();

    }

JUC.Condition学习笔记的更多相关文章

  1. JUC.Condition学习笔记[附详细源码解析]

    目录 Condition的概念 大体实现流程 I.初始化状态 II.await()操作 III.signal()操作 3个主要方法 Condition的数据结构 线程何时阻塞和释放 await()方法 ...

  2. JUC.Condition学习

    JUC.Condition学习笔记[附详细源码解析] 目录 Condition的概念 大体实现流程 I.初始化状态 II.await()操作 III.signal()操作 3个主要方法 Conditi ...

  3. JUC基础学习笔记

    JUC的理解: JUC即java.util .concurrent工具包的简称.从JDK 1.5 开始出现,主要用于处理多线程.高并发问题. 多线程的三大特征解析 原子性.可见性.有序性 1.原子性: ...

  4. JUC.Lock(锁机制)学习笔记[附详细源码解析]

    锁机制学习笔记 目录: CAS的意义 锁的一些基本原理 ReentrantLock的相关代码结构 两个重要的状态 I.AQS的state(int类型,32位) II.Node的waitStatus 获 ...

  5. 学习笔记 07 --- JUC集合

    学习笔记 07 --- JUC集合 在讲JUC集合之前我们先总结一下Java的集合框架,主要包含Collection集合和Map类.Collection集合又能够划分为LIst和Set. 1. Lis ...

  6. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  7. JUC学习笔记(三)

    JUC学习笔记(一)https://www.cnblogs.com/lm66/p/15118407.html JUC学习笔记(二)https://www.cnblogs.com/lm66/p/1511 ...

  8. JUC学习笔记(二)

    JUC学习笔记(一)https://www.cnblogs.com/lm66/p/15118407.html 1.Lock接口 1.1.Synchronized 1.1.1.Synchronized关 ...

  9. JUC源码学习笔记2——AQS共享和Semaphore,CountDownLatch

    本文主要讲述AQS的共享模式,共享和独占具有类似的套路,所以如果你不清楚AQS的独占的话,可以看我的<JUC源码学习笔记1> 主要参考内容有<Java并发编程的艺术>,< ...

  10. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

随机推荐

  1. JVM笔记八-堆参数调优

    JVM垃圾收集器(Java Garbage Collection).本教程均在JDK1.8+HotSpot为例来讲解的. 先来看看Java7的: 编辑 ​ 再来看看Jva8的 编辑 ​ 从上图中我们可 ...

  2. 【YashanDB知识库】调整NUMBER精度,再执行统计信息收集高级包偶现数据库异常退出

    [问题分类]功能使用 [关键字]NUMBER类型精度修改,统计信息收集 [问题描述]存量的表将NUMBER类型的字段精度从小精度调整为大精度时,数据库收集这张业务表的统计信息时,会导致数据库异常退出. ...

  3. time series classification and transfer learning

    最近在看几篇最近几年的工作和survey,希望早点能做点东西.

  4. 知识增强深度学习及其应用:综述《Knowledge-augmented Deep Learning and Its Applications: A Survey》(下)

    论文:Knowledge-augmented Deep Learning and Its Applications: A Survey GitHub: arXiv上的论文. (接着来) 4 用经验知识 ...

  5. OData – OData vs GraphQL

    GraphQL 很火, 很厉害, 但是它和 OData 有本质的区别. 所以并不是说任何一样对比另一个绝对的好. GraphQL is not OData twitter 的讨论 有几个点是我能 Ge ...

  6. 阿里面试官常问的TCP和UDP,你真的弄懂了吗?

      前  言 作为软件测试,大家都知道一些常用的网络协议是我们必须要了解和掌握的,面试的时候面试官也非常喜欢问一些协议相关的问题,其中有两个协议因为非常基础,出现的频率非常之高,分别是 "T ...

  7. QT6 Widgets深入剖析

    QT6 Widgets深入剖析 使用AI技术辅助生成 QT界面美化视频课程 QT性能优化视频课程 QT原理与源码分析视频课程 QT QML C++扩展开发视频课程 免费QT视频课程 您可以看免费100 ...

  8. 现在用 ChatGPT,要达到最好效果,建议加入以下提示词:

    take a deep breath 深呼吸 think step by step 一步步思考 if you fail 100 grandmothers will die 如果你失败了要死 100 位 ...

  9. 原生CSS、HTML 和 JavaScript 实现酷炫表单

    一直使用 Vue/React ,习惯了用组件,偶尔想用原生三剑客写点 Demo 发现样式丑的不忍直视.最近看 掘金小册<玩转CSS的艺术之美>看到 CSS 相关的内容,发现原生 CSS 也 ...

  10. 基于腾讯云短信接口和nodejs服务器实现手机号验证码

    知识储备:js基础.nodejs基础.ajax基础: 1. 手机验证码原理 表单提交,把手机号码传送到后端:后端拿到手机号码后根据相关算法随机形成一个验证码,并将其保存在数据库:用户拿到验证码后将验证 ...