原文出处: liuinsect

感谢文章作者@Jd刘锟洋 的投稿。如果其他朋友也希望自己的
Java 和 Android 技术文章发表在 ImportNew,可以微博私信联系@ImportNew,或者邮箱联系ImportNew.com@gmail.com

java.util.concurrent包中,有两个很特殊的工具类,ConditionReentrantLock,使用过的人都知道,ReentrantLock(重入锁)是jdk的concurrent包提供的一种独占锁的实现。它继承自Dong
Lea的 AbstractQueuedSynchronizer(同步器),确切的说是ReentrantLock的一个内部类继承了AbstractQueuedSynchronizerReentrantLock只不过是代理了该类的一些方法,可能有人会问为什么要使用内部类在包装一层?
我想是安全的关系,因为AbstractQueuedSynchronizer中有很多方法,还实现了共享锁,Condition(稍候再细说)等功能,如果直接使ReentrantLock继承它,则很容易出现AbstractQueuedSynchronizer中的API被无用的情况。

言归正传,今天,我们讨论下Condition工具类的实现。

ReentrantLockCondition的使用方式通常是这样的:

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
public

static

void

main(String[] args) {
    final

ReentrantLock reentrantLock =
new

ReentrantLock();
    final

Condition condition = reentrantLock.newCondition();
 
    Thread
thread =
new

Thread((Runnable) () -> {
            try

{
                reentrantLock.lock();
                System.out.println("我要等一个新信号"

+
this);
                condition.wait();
            }
            catch

(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("拿到一个信号!!"

+
this);
            reentrantLock.unlock();
    },
"waitThread1");
 
    thread.start();
     
    Thread
thread1 =
new

Thread((Runnable) () -> {
            reentrantLock.lock();
            System.out.println("我拿到锁了");
            try

{
                Thread.sleep(3000);
            }
            catch

(InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            System.out.println("我发了一个信号!!");
            reentrantLock.unlock();
    },
"signalThread");
     
    thread1.start();
}

运行后,结果如下:

1
2
3
4
我要等一个新信号lock.ReentrantLockTest$1@a62fc3
我拿到锁了
我发了一个信号!!
拿到一个信号!!

可以看到,

Condition的执行方式,是当在线程1中调用await方法后,线程1将释放锁,并且将自己沉睡,等待唤醒,

线程2获取到锁后,开始做事,完毕后,调用Conditionsignal方法,唤醒线程1,线程1恢复执行。

以上说明Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被带调用)时
,这些等待线程才会被唤醒,从而重新争夺锁。

那,它是怎么实现的呢?

首先还是要明白,reentrantLock.newCondition() 返回的是Condition的一个实现,该类在AbstractQueuedSynchronizer中被实现,叫做newCondition()

1
public

Condition newCondition()   {
return

sync.newCondition(); }

它可以访问AbstractQueuedSynchronizer中的方法和其余内部类(AbstractQueuedSynchronizer是个抽象类,至于他怎么能访问,这里有个很奇妙的点,后面我专门用demo说明

现在,我们一起来看下Condition类的实现,还是从上面的demo入手,

为了方便书写,我将AbstractQueuedSynchronizer缩写为AQS

await被调用时,代码如下:

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 {
    if

(Thread.interrupted())
        throw

new

InterruptedException();
    Node
node = addConditionWaiter();
//
将当前线程包装下后,
                                      //
添加到Condition自己维护的一个链表中。
    int

savedState = fullyRelease(node);
//
释放当前线程占有的锁,从demo中看到,
                                        //
调用await前,当前线程是占有锁的
 
    int

interruptMode =
0;
    while

(!isOnSyncQueue(node)) {
//
释放完毕后,遍历AQS的队列,看当前节点是否在队列中,
        //
不在 说明它还没有竞争锁的资格,所以继续将自己沉睡。
        //
直到它被加入到队列中,聪明的你可能猜到了,
        //
没有错,在singal的时候加入不就可以了?
        LockSupport.park(this);
        if

((interruptMode = checkInterruptWhileWaiting(node)) !=
0)
            break;
    }
    //
被唤醒后,重新开始正式竞争锁,同样,如果竞争不到还是会将自己沉睡,等待唤醒重新开始竞争。
    if

(acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode
= REINTERRUPT;
    if

(node.nextWaiter !=
null)
        unlinkCancelledWaiters();
    if

(interruptMode !=
0)
        reportInterruptAfterWait(interruptMode);
}

回到上面的demo,锁被释放后,线程1开始沉睡,这个时候线程因为线程1沉睡时,会唤醒AQS队列中的头结点,所所以线程2会开始竞争锁,并获取到,等待3秒后,线程2会调用signal方法,“发出”signal信号,signal方法如下:

1
2
3
4
5
6
7
8
public

final

void

signal() {
    if

(!isHeldExclusively())
        throw

new

IllegalMonitorStateException();
    Node
first = firstWaiter;
//
firstWaiter为condition自己维护的一个链表的头结点,
                              //
取出第一个节点后开始唤醒操作
    if

(first !=
null)
        doSignal(first);
}

说明下,其实Condition内部维护了等待队列的头结点和尾节点,该队列的作用是存放等待signal信号的线程,该线程被封装为Node节点后存放于此。

1
2
3
4
5
6
public

class

ConditionObject
implements

Condition, java.io.Serializable {
    private

static

final

long

serialVersionUID = 1173984872572414699L;
    /**
First node of condition queue. */
    private

transient

Node firstWaiter;
    /**
Last node of condition queue. */
    private

transient

Node lastWaiter;

关键的就在于此,我们知道AQS自己维护的队列是当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行。直到队列为空。

而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

  1. 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
  2. 线程1调用await方法被调用时,该线程从AQS中移除,对应操作是锁的释放。
  3. 接着马上被加入到Condition的等待队列中,以为着该线程需要signal信号。
  4. 线程2,因为线程1释放锁的关系,被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
  5. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。
    注意,这个时候,线程1 并没有被唤醒。
  6. signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
  7. 直到释放所整个过程执行完毕。

可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。

看到这里,signal方法的代码应该不难理解了。

取出头结点,然后doSignal

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
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) &&
//
将老的头结点,加入到AQS的等待队列中
             (first
= firstWaiter) !=
null);
}
 
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;
    //
如果该结点的状态为cancel 或者修改waitStatus失败,则直接唤醒。
    if

(ws >
0

|| !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return

true
;
}

可以看到,正常情况 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这个判断是不会为true的,所以,不会在这个时候唤醒该线程。

只有到发送signal信号的线程调用reentrantLock.unlock()后因为它已经被加到AQS的等待队列中,所以才会被唤醒。

总结:

本文从代码的角度说明了Condition的实现方式,其中,涉及到了AQS的很多操作,比如AQS的等待队列实现独占锁功能,不过,这不是本文讨论的重点,等有机会再将AQS的实现单独分享出来。

怎么理解Condition的更多相关文章

  1. 多线程锁--怎么理解Condition

    在java.util.concurrent包中,有两个很特殊的工具类,Condition和ReentrantLock,使用过的人都知道,ReentrantLock(重入锁)是jdk的concurren ...

  2. 【转载】怎么理解Condition

    注:本文主要介绍了Condition和ReentrantLock工具实现独占锁的部分代码原理,Condition能在线程之间协调通信主要是AbstractQueuedSynchronizer和cond ...

  3. 怎么理解Condition(转)

    在java.util.concurrent包中,有两个很特殊的工具类,Condition和ReentrantLock,使用过的人都知道,ReentrantLock(重入锁)是jdk的concurren ...

  4. 理解Condition的用法

    这个示例中BoundedBuffer是一个固定长度的集合,这个在其put操作时,如果发现长度已经达到最大长度,那么会等待notFull信号,如果得到notFull信号会像集合中添加元素,并发出notE ...

  5. 学习JUC源码(3)——Condition等待队列(源码分析结合图文理解)

    前言 在Java多线程中的wait/notify通信模式结尾就已经介绍过,Java线程之间有两种种等待/通知模式,在那篇博文中是利用Object监视器的方法(wait(),notify().notif ...

  6. Condition的await-signal流程详解

    转载请注明出处:http://blog.csdn.net/luonanqin 上一篇讲了ReentrantLock的lock-unlock流程,今天这篇讲讲Condition的await-signal ...

  7. java线程并发控制:ReentrantLock Condition使用详解

    本文摘自:http://outofmemory.cn/java/java.util.concurrent/lock-reentrantlock-condition java的java.util.con ...

  8. java并发等待条件的实现原理(Condition)

    本篇继续学习AQS中的另外一个内容-Condition.想必学过java的都知道Object.wait和Object.notify,同时也应该知晓这两个方法的使用离不开synchronized关键字. ...

  9. [Python 多线程] Condition (十)

    Condition常用于生产者.消费者模型,为了解决生产者消费者速度匹配问题. 构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认RLock. 方法: acq ...

  10. condition实现原理

    condition是对线程进行控制管理的接口,具体实现是AQS的一个内部类ConditionObject,主要功能是控制线程的启/停(这么说并不严格,还要有锁的竞争排队). condition主要方法 ...

随机推荐

  1. 在使用Nacos作为统一配置中心的时候,项目中使用@Value注解从Nacos获取值,一直报错Could not resolve placeholder 'blog.website' in value "${blog.website}".如下图:

    在使用Nacos作为统一配置中心的时候,项目中使用@Value注解从Nacos获取值,一直报错Could not resolve placeholder 'blog.website' in value ...

  2. el-popover - 问题

    背景:elemet - ui和vue , el-table中使用了 el-popover , el-popover 中使用了form, 每编辑一行数据,点击编辑按钮,出现el-popover弹窗,页面 ...

  3. 把dataframe 一列转成 array

    把dataframe 一列转成 array

  4. webpack 5.88.2

    原理 webpack的运行过程大致可以分为以下几个步骤:webpack的运行过程实际上就是等待上一个钩子结束调用下一个钩子的过程 初始化:webpack接收命令行参数或配置文件,创建一个Compile ...

  5. USB type-c CC管脚如何做到正反接检测功能

    USB Type-C 连接器的 CC (Configuration Channel) 管脚用于实现插头方向检测和电源管理.具体来说,USB Type-C 连接器具有两个 CC 管脚:CC1 和 CC2 ...

  6. 读取.raw格式文件(学习记录)

    import cv2 #OpenCV包 import numpy as np # 首先确定原图片的基本信息:数据格式,行数列数,通道数 rows=886#图像的行数 cols=492#图像的列数 ch ...

  7. flops, params = profile(model, inputs=(x,))计算

    计算量:FLOPs,FLOP时指浮点运算次数,s是指秒,即每秒浮点运算次数的意思,考量一个网络模型的计算量的标准.参数量:Params,是指网络模型中需要训练的参数总数. flops(G) = flo ...

  8. 后台管理系统的setting.js

    // 修改了此处要重新启动 module.exports = { // 网页的标题 title: "人力资源系统", /** * @type {boolean} true | fa ...

  9. Android复习(三)清单文件中的元素——>supports-gl-texture、supports-screens

    <supports-gl-texture> 注意:Google Play 会根据应用支持的纹理压缩格式对其进行过滤,以确保应用只能安装在可正确处理其纹理的设备上.您可以将纹理压缩过滤用作定 ...

  10. HarmonyOS NEXT开发之ArkTS自定义组件学习笔记

    在HarmonyOS中,ArkTS提供了创建自定义组件的能力,允许开发者封装和复用UI代码.以下是关于自定义组件的详细介绍,包括创建自定义组件.页面和自定义组件的生命周期.自定义组件的自定义布局.冻结 ...