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

目录

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 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
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 
1
2
3
4
5
6
7
 
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

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

Java Code 
1
2
3
4
5
6
7
8
 
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 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
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 
1
2
3
4
5
6
7
8
9
 
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 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
 
/**
 * 生产者、消费者示例
 */
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();
    }

原创文章,请注明引用来源:CM4J
参考文章列表:
http://www.goldendoc.org/2011/06/juc_condition/
http://www.blogjava.net/xylz/archive/2010/07/08/325540.html

posted on 2013-08-28 17:19 CM4J 阅读(...) 评论(...) 编辑 收藏

转载于:https://www.cnblogs.com/cm4j/p/juc_condition.html

JUC.Condition学习笔记[附详细源码解析]相关推荐

  1. K8s基础知识学习笔记及部分源码剖析

    K8s基础知识学习笔记及部分源码剖析 在学习b站黑马k8s视频资料的基础上,查阅了配套基础知识笔记和源码剖析,仅作个人学习和回顾使用. 参考资料: 概念 | Kubernetes 四层.七层负载均衡的 ...

  2. 通俗易懂玩QT:Qpaint绘制开关按钮(内附详细源码)

    Qpaint 绘制开关按钮(内附详细源码) 一.实验效果与开发环境 效果图如下: 开发环境: 二.实验代码 switch.h #ifndef SWITCH_H #define SWITCH_H#inc ...

  3. 实战|Python轻松实现动态网页爬虫(附详细源码)

    用浅显易懂的语言分享爬虫.数据分析及可视化等干货,希望人人都能学到新知识. 项目背景 事情是这样的,前几天我公众号写了篇爬虫入门的实战文章,叫做<实战|手把手教你用Python爬虫(附详细源码) ...

  4. beautifulsoup解析动态页面div未展开_实战|Python轻松实现动态网页爬虫(附详细源码)...

    用浅显易懂的语言分享爬虫.数据分析及可视化等干货,希望人人都能学到新知识.项目背景事情是这样的,前几天我公众号写了篇爬虫入门的实战文章,叫做<实战|手把手教你用Python爬虫(附详细源码)&g ...

  5. 自动化测试如何保持登录状态_自动化测试po模式是什么?自动化测试po分层如何实现?-附详细源码...

    一.什么是PO模式 全称:page object model 简称:POM/PO PO模式最核心的思想是分层,实现松耦合!实现脚本重复使用,实现脚本易维护性! 主要分三层: 1.基础层BasePage ...

  6. Python编程:实现词云生成(附详细源码)

    Python编程:实现词云生成(附详细源码) 词云是一种数据可视化的方式,它可以用来展示某个主题下的主要关键词汇.在Python中,我们可以使用 wordcloud 库来实现词云的生成.本文将带您一步 ...

  7. weiler-atherton多边形裁剪算法_EAST算法超详细源码解析:数据预处理与标签生成...

    作者简介 CW,广东深圳人,毕业于中山大学(SYSU)数据科学与计算机学院,毕业后就业于腾讯计算机系统有限公司技术工程与事业群(TEG)从事Devops工作,期间在AI LAB实习过,实操过道路交通元 ...

  8. EAST算法超详细源码解析:数据预处理与标签生成

    作者简介 CW,广东深圳人,毕业于中山大学(SYSU)数据科学与计算机学院,毕业后就业于腾讯计算机系统有限公司技术工程与事业群(TEG)从事Devops工作,期间在AI LAB实习过,实操过道路交通元 ...

  9. 【Java学习002】Java-ArrayList源码解析

    ArrayList源码解析 1.1底层数据结构 定义:实现List接口的可扩容数组实现. 数组特点: 查询快:数组开辟的是连续空间,所以可以依靠索引进行快速查询. 增删慢:每次删除元素,都需要更改数组 ...

最新文章

  1. java中用byte[]数组实现的队列和用Byte[]实现的队列实际占用空间对比
  2. 长短期记忆(LSTM)相关知识
  3. leetcode双指针合集
  4. 编程实现将rdd转换为dataframe:源文件内容如下(_第四篇|Spark Streaming编程指南(1)
  5. sim7600ce 拨号上网测试_树莓派系列教程:通过SIM7600 4G模块NDIS拨号
  6. content的定义
  7. 搜索推荐广告中的Position Bias:美团DPIN
  8. 优雅的封装ajax,含跨域
  9. AfterEffects 不支持 MKV 格式的解决办法
  10. js:聚焦和失焦事件示例
  11. 计算机用户域怎么删除,如何删除域内非活动计算机账号?
  12. 详解java的垃圾清理机制
  13. Excel的图表:组成元素、图表类型与用途、图表可视化大全
  14. Mac 下生成keystore以及获得数字签名
  15. 清默网络——CCIE考试经验与心得(1)
  16. 09-word不显示段落标记(去掉回车符号)取消拼写错误
  17. Microbit试题
  18. win7怎么设置计算机的性能,windows7旗舰版电脑如何为电脑设置高性能计划
  19. 如何下载离线地图金字塔瓦片数据
  20. Smart-Link配置

热门文章

  1. C语言:函数中参数的传值与传地址
  2. C++文件打开模式详解
  3. 为什么判断 n 是否为质数只需除到开平方根就行了?(直接证明)
  4. 手把手教你安装VMtools
  5. c语言随机迷宫生成方法,[原创]递归随机迷宫生成算法详解
  6. struts2 form标签加上validate=true就出错的解决办法
  7. Finacial professional
  8. tomcat默认用户名密码修改
  9. LeetCode-反转链表
  10. 具名元祖--namedtuple