一、Condition接口

1、Condition接口的常用方法介绍

 /**
* 已经获取到锁的线程调用该方法会进入等待状态,知道其他持有锁的线程通知(signal)等待队列中的线程或者被中断退出等待队列;
* 如果该线程已经从该方法中返回,表名线程已经获取到了Condition对象对应的锁
*/
public final void await() throws InterruptedException {...}
/**
* 还是进入等待状态的方法,只是该方法对中断不敏感:当前调用该方法的线程只有被通知(signal)才能从等待队列中退出
*/
public final void awaitUninterruptibly() {...}
/**
* 当前线程进入等待状态,被通知、中断或者超时之后被唤醒。返回值就是表示剩余的时间,即
* 如果在nanosTimeout纳秒之前被唤醒,返回值就是实际耗时;如果返回值是0或者负数,就认为是超时了
*/
public final long awaitNanos(long nanosTimeout) {...}
/**
* 调用该方法的线程会进入等待状态直到被通知、中断或者到达某个超时时间。
* 意味着没有到达指定的某个时间被通知,就会返回true;如果到达指定时间,返回false
*/
public final boolean awaitUntil(Date deadline){}
/**
* 当前持有Condition对象对应锁的线程,调用该方法之后会唤醒一个等待在Condition上的线程
*/
public final void signal() {}
/**
* 当前持有Condition对象对应锁的线程,调用该方法之后会唤醒等待在Condition上的所有线程
*/
public final void signalAll() {}

  Condition的使用模板:Condition的获取必须通过Lock的newCondition方法,表示Condition对象与该锁关联,一般讲Condition对象作为成员变量,调用上面的await方法之后当前线程才会释放锁并在等待队列中进行等待;当其他的线程(在没有中断的情况下)调用该condition对象的signal方法的时候就会通知等待队列中的等待线程从await方法返回(返回之前已经获取锁)。

 Lock lock = new ReentrantLock();
Condition con = lock.newCondition();
public void conWait() {
lock.lock();
try {
con.await();
} catch(InterruptedException e) {
...
}finally {
lock.unlock();
}
} public void conSignal() {
lock.lock();
try {
con.signal();
} catch(InterruptedException e) {
...
}finally {
lock.unlock();
}
}

2、Condition的实现分析

a)源码流程分析

  我们通过跟踪源码可以看出来,首先创建锁对象(new ReentrantLock()),然后根据锁对象关联响应的Condition对象,然后通过Condition对象中维护的等待队列实现等待(await)通知(signal)机制。

 public Condition newCondition() { //ReentrantLock类中的方法
return sync.newCondition();
}
//ConditionObject类实现Condition接口,除此室外ConditionObject也是AQS的一个内部类,Condition的操作需要与锁关联起来
final ConditionObject newCondition() {
return new ConditionObject();
}
//AQS的内部类ConditionObject,其中维护了一个等待队列,通过该队列实现等待通知机制
public class ConditionObject{
/**
* 返回等待队列中的线程集合
* @throws IllegalMonitorStateException if {@link #isHeldExclusively}
* returns {@code false}
*/
protected final Collection<Thread> getWaitingThreads() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node w = firstWaiter; w != null; w = w.nextWaiter) {
if (w.waitStatus == Node.CONDITION) {
Thread t = w.thread;
if (t != null)
list.add(t);
}
}
return list;
}
}

b)具体实现

  上面说到了Condition是通过等待队列来实现等待通知功能的,那么就分析等待队列和等待通知机制的实现

①等待队列实现

  等待队列是一个FIFO的队列,其中每个结点都包含一个处于Condition对象上等待的线程引用(当一个获取到锁的线程调用await方法,就会释放锁资源,被包装成一个Node然后添加到等待队列中进入等待状态;这里面的Node结点还是和AQS中的实现机理一样,Node是AQS中的静态内部类)。

  ConditionObject类中有下面两个属性,分别代表一个Condition对应的等待队列的首节点和尾结点。当前线程调用await方法之后就会被构造成一个Node结点然后加入到等待队列的尾部。

 /** Condition等待队列头结点 */
private transient Node firstWaiter;
/** Condition等待队列尾结点 */
private transient Node lastWaiter;

  下面是等待队列的基本结构,Condition对象中有首尾结点的引用。新增加的结点需要将原有的尾结点的下一节点指向它,然后更新lastWaiter即可。

  上面的情况是一个Condition对象对应一个等待队列和一个同步队列(上面新添加的Node3就是从同步队列中移除然后添加过来的),在同步器组件实现中,会拥有一个同步队列和多个等待队列。

②等待操作的实现

  持有锁的线程调Condition的await方法之后会释放锁,然后进入等待状态。既然是持有锁的线程,那么该线程应该位于同步队列的首节点位置,其调用await方法之后就会从同步队列首节点移到等待队列的尾结点等待。具体将其移到等待队列是addConditionWaiter方法实现。下面是await方法和addConditionWaiter方法的实现分析。

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;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
/**
* waitStatus值表示线程正在等待条件(原本结点在等待队列中,结点线程等待在Condition上,当其他线程对
* Condition调用了signal()方法之后)该结点会从等待队列中转移到同步队列中,进行同步状态的获取
* static final int CONDITION = -2;
*/
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); //构造成Condition的等待队列中的对应的结点
//增加的结点需要将原有的尾结点的下一节点指向它,然后更新lastWaiter
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}

③通知操作的实现

  通知操作的实现机制就是将当前等待队列中的首节点中的线程唤醒,将其加入同步队列中。

 public final void signal() {
if (!isHeldExclusively()) //检查当前线程是否获取锁
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}

  唤醒线程使其进入同步队列之后,我们再来看await方法中那些没有执行的代码。

 public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); //将当前线程加入等待队列
int savedState = fullyRelease(node); //释放当前线程持有的锁
int interruptMode = 0;
//根据下面的源码可以看出,当前线程如果掉用await方法之后会进入等待队列,那么在退出等待队列之前会一直执行这个循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this); //唤醒节点中的线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//通过acquireQueued源码可以发现,获取锁的流程和ReentrantLock这种独占式获取同步状态的流程基本一致
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null) //判断当前队列是否在等待队列中
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
//竞争锁资源的同步队列
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); //得到当前结点的前驱结点
if (p == head && tryAcquire(arg)) { //当前结点的前驱结点为头结点,并且尝试获取锁成功
setHead(node); //将当前获取到锁的结点设置为头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果获取同步状态失败,应该自旋等待继续获取并且校验自己的中断标志位信息
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  从上面的代码中我们可以看出,当调用await方法的线程在没有回到同步队列之前,都会一直在while (!isOnSyncQueue(node)){...}循环中,只有被唤醒退出等待队列进入同步队列才会从循环中退出;之后调用acquireQueued()开始自旋等待锁的获取,这个自旋的过程和前面介绍的AQS中独占式锁的获取流程一样;最后,如果线程从这个自旋的过程退出了,就代表当前线程再次获取了锁资源,最后也从await方法返回。所以,一个线程调用await方法之后,只有最终获取到锁才会从该方法返回。而对于signalAll而言就是对等待队列中的每个线程通知(signal)一次,这样就可以将等待队列中的所有线程移到同步队列中进行锁资源的获取。

二、Condition接口使用

1、Condition接口搭配ReentrantLock实现生产者消费者模式

 package cn.source.condition;

 import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class ConditionProducerAndConsumer<E> { private LinkedList<E> list = new LinkedList<E>();
private static final int MAX_NUM = 10; //容器的最大数量
private int count = 0; //容器中实际数量 private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition(); private int getCount() {
return count;
} private void put(E e) {
lock.lock(); //首先需要获取锁
try {
//这里是判断容器是否已满,注意需要使用while:如果使用if的话可能导致所有的消费线程都处于等待状态
while(list.size() == MAX_NUM) {
System.out.println(Thread.currentThread().getName() + "正在等待中");
producer.await(); //生产者线程进入等待状态
}
//添加元素
list.add(e);
count ++;
consumer.signalAll();//将消费者线程唤醒
} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
lock.unlock();
}
} private E get() {
E e = null;
lock.lock();
try {
while(list.size() == 0) {
System.out.println(Thread.currentThread().getName() + "正在等待");
consumer.await(); //消费者线程进入等待状态
}
e = list.removeFirst();
count --;
producer.signalAll(); //消费元素之后,将生产者线程唤醒
} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
lock.unlock();
}
return e;
} public static void main(String[] args) {
SyncProducerAndConsumer<String> syncProducerAndConsumer = new SyncProducerAndConsumer<>();
for (int i = 0; i < 10; i++) { //开启10个线程
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) { //每个线程从容器中获取5次数据
System.out.println(syncProducerAndConsumer.get());
}
} }, "消费者线程" + i).start();;
}
//休眠2秒,所有的消费者线程都已经启动并且处于等待状态
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} for (int i = 0; i < 2; i++) { //开启两个生产者线程
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 25; j++) { //每个生产者线程想容器中添加25个数据,当容器中数据到达10个的时候生产者线程会阻塞
syncProducerAndConsumer.put("add value " + j);
}
}
}, "生产者线程"+i).start();
}
} }

2、synchronized组合wait/notify实现生产者消费者模式

 package cn.source.condition;

 import java.util.LinkedList;
import java.util.concurrent.TimeUnit; public class SyncProducerAndConsumer<E> { private LinkedList<E> list = new LinkedList<E>();
private static final int MAX_NUM = 10; //容器的最大数量
private int count = 0; //容器中实际数量 public synchronized int getCount() {
return count;
} public synchronized void put(E e) {
while(list.size() == MAX_NUM) { //这里是判断容器是否已满,注意需要使用while:如果使用if的话可能导致所有的消费线程都处于等待状态
try {
this.wait(); //容器满了之后,生产者线程进入等待状态
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
//容器未满,生产者线程就想容器中添加数据
list.add(e);
count ++;
this.notifyAll(); //此时容器中已经存在数据,唤醒等待的消费者线程
} public synchronized E get() {
E e = null;
while(list.size() == 0) { //判断容器是否为空,如果为空就进入等待状态,这里也使用while
try {
this.wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
e = list.removeFirst();
count --;
this.notifyAll();
return e;
} public static void main(String[] args) {
SyncProducerAndConsumer<String> syncProducerAndConsumer = new SyncProducerAndConsumer<>();
for (int i = 0; i < 10; i++) { //开启10个线程
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) { //每个线程从容器中获取5次数据
System.out.println(syncProducerAndConsumer.get());
}
} }, "消费者线程" + i).start();;
}
//休眠2秒,所有的消费者线程都已经启动并且处于等待状态
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} for (int i = 0; i < 2; i++) { //开启两个生产者线程
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 25; j++) { //每个生产者线程想容器中添加25个数据,当容器中数据到达10个的时候生产者线程会阻塞
syncProducerAndConsumer.put("add value " + j);
}
}
}, "生产者线程"+i).start();
}
} }

3、Object中的等待唤醒机制和Condition的等待通知机制对比

Java中的线程协作之Condition的更多相关文章

  1. Java中的线程--Lock和Condition实现线程同步通信

    随着学习的深入,我接触了更多之前没有接触到的知识,对线程间的同步通信有了更多的认识,之前已经学习过synchronized 实现线程间同步通信,今天来学习更多的--Lock,GO!!! 一.初时Loc ...

  2. Java多线程之线程协作

    Java多线程之线程协作 一.前言 上一节提到,如果有一个线程正在运行synchronized 方法,那么其他线程就无法再运行这个方法了.这就是简单的互斥处理. 假如我们现在想执行更加精确的控制,而不 ...

  3. Java中的线程到底有哪些安全策略

    摘要:Java中的线程到底有哪些安全策略呢?本文就为你彻底分析下! 本文分享自华为云社区<[高并发]线程安全策略>,作者:冰 河 . 一.不可变对象 不可变对象需要满足的条件 (1)对象创 ...

  4. Java中的线程

    http://hi.baidu.com/ochzqvztdbabcir/item/ab9758f9cfab6a5ac9f337d4 相濡以沫 Java语法总结 - 线程 一 提到线程好像是件很麻烦很复 ...

  5. [译]线程生命周期-理解Java中的线程状态

    线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...

  6. Java中的线程Thread总结

    首先来看一张图,下面这张图很清晰的说明了线程的状态与Thread中的各个方法之间的关系,很经典的! 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口. 要注意的是Threa ...

  7. JAVA中创建线程的三种方法及比较

    JAVA中创建线程的方式有三种,各有优缺点,具体如下: 一.继承Thread类来创建线程 1.创建一个任务类,继承Thread线程类,因为Thread类已经实现了Runnable接口,然后重写run( ...

  8. 浅谈利用同步机制解决Java中的线程安全问题

    我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等 ...

  9. 第9章 Java中的线程池 第10章 Exector框架

    与新建线程池相比线程池的优点 线程池的分类 ThreadPoolExector参数.执行过程.存储方式 阻塞队列 拒绝策略 10.1 Exector框架简介 10.1.1 Executor框架的两级调 ...

随机推荐

  1. Django—Form、ModelForm

    一.Form form.py from django import forms from django.core.exceptions import ValidationError from djan ...

  2. bzoj 2599

    还是点对之间的问题,果断上点分治 同样,把一条路径拆分成经过根节点的两条路径,对不经过根节点的路径递归处理 然后,我们逐个枚举根节点的子树,计算出子树中某一点到根节点的距离,然后在之前已经处理过的点中 ...

  3. 末学者笔记--NTP服务和DNS服务

    NTP时间服务器 一.概念: 作用:ntp主要是用于对计算机的时间同步管理操作. 时间是对服务器来说是很重要的,一般很多网站都需要读取服务器时间来记录相关信息,如果时间不准,则可能造成很大的影响. 二 ...

  4. oh-my-zsh: 让终端飞

    上一次推文写了JupyterLab:程序员的笔记本神器,介绍的是如何在web端打造一个便捷的开发环境,发出后反响还不错 因此我决定再写几篇能提升程序员工作以及学习效率的文章,如果能形成一个系列那是最好 ...

  5. C# DataConstruct 数据结构关于 Array,ArrayList,List,HashTable,Dictionnary的学习记录

    Array: 数组,开辟连续存储的内存存储数据.Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据 优点: 1.查询速度快,可以利用索引快速查询到对 ...

  6. [转]impala操作hive数据实例

    https://blog.csdn.net/wiborgite/article/details/78813342 背景说明: 基于CHD quick VM环境,在一个VM中同时包含了HDFS.YARN ...

  7. oracle数据库学习

    trunc(number[,decimals])--number 待做截取处理的数值:decimals 指明需保留小数点后面的位数     CREATE PUBLIC DATABASE LINK Co ...

  8. 放球游戏B

    题目描述 校园里在上活动课,Red和Blue两位小朋友在玩一种游戏,他俩在一排N个格子里,自左到右地轮流放小球,每个格子只能放一个小球.第一个人只能放1个球,之后的人最多可以放前一个人的两倍数目的球, ...

  9. E - Elevator

    E - Elevatorhttp://codeforces.com/gym/241680/problem/E同余最短路,从0~a-1中每一个i向(i+b)%a连一条权值为b的边,向(i+c)%a连一条 ...

  10. PCB Mark点相关

    1)Mark点用于锡膏印刷和元件贴片时的光学定位.根据Mark点在PCB上的作用,可分为拼板Mark点.单板Mark点.局部Mark点(也称器件级MARK点) 2)拼板的工艺边上和不需拼板的单板上应至 ...