上篇文章我们介绍了synchronized关键字,使用它可以有效的解决我们多线程所带来的一些常见问题。例如:竞态条件,内存可见性等。并且,我们也说明了该关键字主要是一个加锁和释放锁的集成,所有为能获得锁的线程都将被阻塞在某个对象的阻塞队列上。而我们本篇将要介绍的线程间的协作则主要是对对象的另一个队列的使用(条件队列),所有因条件不满足而无法继续运行的线程都将在条件队列上进行等待。主要涉及内容如下:

  • 理解wait/notify这两个方法
  • 典型的生产者消费者问题
  • 理解join方法的实现原理

一、理解wait/notify这两个方法

     这两个方法是我们本篇文章的主角,它们被定义在根类Object中。

public final void wait()
public final native void wait(long timeout) public final native void notify();
public final native void notifyAll();

两个wait方法,无参的wait相当于wait(0)表示无限期等待,有参数的wait方法则指定该线程等待多长时间。notify方法用于释放一个在条件队列上等待的线程,而notifyall方法则是用于释放所有在条件队列上进行等待的线程。那么究竟什么时候调用wait方法让线程到条件队列上去等待,什么时候调用notify释放条件队列上的线程呢?

我们说过一个对象有一把锁和两个队列,对于所有无法获取到锁的线程都将被阻塞在阻塞队列上,而对于获取到锁以后,于运行过程中由于缺少某些条件而不得不终止程序的线程将被阻塞在条件队列上并让出CPU。而且需要注意一点的是,线程被阻塞在阻塞队列上和条件队列上,所表现出的状态是不一样的。例如:

/*定义一个线程类*/
public class MyThread extends Thread{ @Override
public void run(){
try {
System.out.println(Thread.currentThread().getState());
synchronized (this){
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*启动线程*/
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start(); Thread.sleep(1000);
System.out.println(thread.getState());
System.out.println("main is out"); }

输出结果显示:

主函数中启动一个线程,该线程内部运行的时候先输出当前线程状态,然后调用wait方法将自己挂在当前线程对象的条件队列上并让出CPU,而我们在主函数中对该线程的状态进行再一次的输出, 从结果截图来看,程序并没有结束----说明子线程并没有正常结束,阻塞在条件队列上的线程的状态是waiting,这和阻塞在阻塞队列上的线程状态blocked是完全两种不同的状态。但是,当我们调用notify或者notifyall方法将某个线程从条件队列中释放的时候,该线程要和外面的其他线程一样去竞争对象的锁,如果不能获取到对象的锁,依然会被阻塞在该对象的阻塞队列上。

二、使用wait/notify解决生产者消费者问题

     生产者消费者问题是我们操作系统中的一个经典的问题。生产者向仓库中源源不断的放入产品,消费者从仓库中源源不断的拿出产品,当仓库满的时候,生产者就不能继续往里面放入产品,当仓库空的时候,消费者就不能从仓库里取出产品。如何协调好生产者线程和消费者线程对仓库的操作就是这个问题的核心。

public class Repository {

    private ArrayDeque<String> list = null;
private int limit; //仓库容量 public Repository(int limit){
this.limit = limit;
list = new ArrayDeque<String>(this.limit);
} //仓库提供给生产者存入操作
public synchronized void addGoods(String data) throws InterruptedException {
while(list.size() == limit){
//说明仓库已经满了
wait();
}
list.add(data);
System.out.println("i produce a product:"+data);
notifyAll();
} //仓库提供给消费者取出操作
public synchronized String getGoods() throws InterruptedException {
while(list.isEmpty()){
//说明仓库已经空了
wait();
}
String result = list.poll();
System.out.println("i consume a product:"+ result);
notifyAll();
return result;
} }

我们定义一个仓库类,该仓库提供给生产者投放的方法,提供给消费者取出的方法。我们使用双端队列实现对仓库的模拟,limit参数限定仓库容量。

生产者的投放方法,当生产者想要向仓库投放产品时,如果仓库已经满了,则将将当前线程阻塞在条件队列上,等待仓库有空余位置为止。而如果仓库没满,则向其中投入一个产品并唤醒被阻塞在条件队列上的所有线程(在本例中实际上就是消费者线程)。一旦消费者线程从条件队列上被释放,他将重新和生产者线程竞争对象锁,在获取到对象锁之后将回到上次因条件不足而被阻塞的程序位置。消费者的取出方法和生产者的投放方法类似,此处不再赘述。

public class Producer extends Thread {

    //生产者线程不停的生产产品直到仓库满

    private Repository repository = null;

    public Producer(Repository r){
this.repository = r;
} int count = 0;
@Override
public void run(){
while(true){
try {
repository.addGoods(String.valueOf(count));
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

定义一个生产者类,生产者始终不停的生产产品,我们用count来模拟产品代号。

public class Consumer extends Thread {

    //消费者线程不停的从仓库中取出产品直到仓库空

    private Repository repository = null;

    public Consumer(Repository r){
this.repository = r;
} @Override
public void run(){
while(true){
try {
String result = repository.getGoods();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

定义一个消费者类,消费者不停的从仓库中取出产品。

public static void main(String[] args) throws InterruptedException {

        Repository repository = new Repository(20);
Thread producer = new Producer(repository);
Thread consumer = new Consumer(repository); producer.start();
consumer.start(); System.out.println("main thread is out");
}

最后我们定义一个仓库并通过构造方法的传入使得生产者和消费者共同使用相同的仓库对象。分别启动两个线程,程序将死循环的输出生产者和消费者的生产和消费操作,以下是程序运行的部分结果:

我们可以看到生产者和消费者这两个线程交替的输出,偶尔会出现消费者滞后生产者的情况,但是消费者绝对不会超前生产者,因为只有生产者生产出产品之后,消费者才能取出。以上便是经典的生产者消费者问题,通过对该问题的实现,我们能够对wait/notify这两个操作有了一个更加深刻的认识。

三、join方法的实现原理

     join方法的内部其实使用的还是我们上述介绍的wait/notify机制。

public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0; if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

两个方法,核心的还是这个带有参数的join方法。该方法大体上分为三种情况,如果millis小于0,抛出异常。如果millis等于0,就无限期等待,这一段代码不知道大家理解的如何:

if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
Thread thread = new MyThread();
thread.start(); thread.join();

两小段代码,第一段代码是jdk中关于millis 等于0的一个实现,第二段代码则是我们调用join方法的一个基本格式。我们可以看到,由于join这个方法被synchronized关键字修饰,那么我们主线程在调用thread对象的该方法时就需要首先获得thread对象的锁。

进入到join方法的内部,当millis 等于0的时候,判断只要线程对象活着,也就是thread对象活着,就调用wait(0)方法将当前线程(main)线程挂起到thread对象的条件队列上。一旦thread线程对象执行结束,Java系统将调用notifyall来释放所有挂在该对象的条件队列上的线程,此时main线程将会被唤醒,从而实现了main线程等待thread线程执行结束的一个过程。至于millis 大于0的情况,只不过内部调用了wait(long timeout)方法,其他的实现原理基本类似,此处不再赘述。

本篇文章,我们主要介绍线程间的一种协作机制,使用wait/notify两个方法来协作不同的线程。通过实现经典的生产者消费者模型增加了对wait/notify这两个方法的理解,最后从源代码的角度对Thread下的join方法进行了学习,该方法的核心就是利用wait/notify协作主线程和分支线程来实现等待的一个操作。总结不到之处,望指出。

Java并发之线程间的协作的更多相关文章

  1. java并发之线程间通信协作

    在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界 ...

  2. Java并发之线程间的同步协作与通信协作

    1,Monitor监视器与syncrhoized实现原理 1.1:Monitor Monitor是一个同步工具,相当于操作系统中的互斥量(mutex),即值为1的信号量. 它内置与每一个Object对 ...

  3. java并发之线程间通信

    1.volatile 关键字 java 支持多个线程同时访问一个对象或对象的成员变量,而每个线程拥有这个变量的拷贝,虽然对象或成员变量分配的内存在共享内存,但每个执行的线程可以拥有一份拷贝,可以提高程 ...

  4. Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. 【转】Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线 ...

  6. java并发编程(一)线程状态 & 线程中断 & 线程间的协作

    参考文章: Java线程的5种状态及切换:http://blog.csdn.net/pange1991/article/details/53860651 线程的5种状态: 1. 新建(NEW):新创建 ...

  7. Java中详述线程间协作

    线程协作 首先引入一段代码: package 线程间数据共享; import java.util.Date; public class Watch { private static String ti ...

  8. java中关于线程间协作所用关键字synchronized,wait,notify的用法

    wait/notify()关键字适用于一个线程通知另一个线程所需的条件状态已就绪,最常用于线程在循环中休眠直到获取特定条件的场景. 例如,一个线程一直等待直到队列中有一个组件能够处理:当组件添加到队列 ...

  9. Java并发编程:线程间通信wait、notify

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

随机推荐

  1. DRAM(MT48LC8M32B2)学习

    Micron公司,型号为MT48LC8M32B2的DRAM,其86PIN的 TSOP封装引脚定义 Micron公司,型号为MT48LC8M32B2的DRAM,其86PIN的 TSOP封装引脚定义如上图 ...

  2. InvalidateRect和UpdateWindow

    The UpdateWindow function updates the client area of the specified window by sending a WM_PAINT mess ...

  3. jquery的2.0.3版本源码系列(4):285-348行,extend方法详解

    目录 1 . jquery extend的基本使用 通过285行的源码 jQuery.extend = jQuery.fn.extend = function() { ,extend方法要么是直接挂在 ...

  4. ASP.NET 给Web中的网页添加Loading进度条形式

    前段时间客户提了一个需求,要求给网站中某些功能添加进度条形式,因为某些功能查询的数据量太大,经常会出现点击Search按钮,但是没有任何反应的情况,会让用户以为网站挂掉了,导致投诉的事情发生,所以客户 ...

  5. mysql中group by 的用法解析

    1. group by的常规用法 group by的常规用法是配合聚合函数,利用分组信息进行统计,常见的是配合max等聚合函数筛选数据后分析,以及配合having进行筛选后过滤. 假设现有数据库表如下 ...

  6. 设计模式,Let's “Go”! (下)

    * { color: #3e3e3e } body { font-family: "Helvetica Neue", Helvetica, "Hiragino Sans ...

  7. mapreduce运行机制

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt243 谈mapreduce运行机制,可以从很多不同的角度来描述,比如说从ma ...

  8. 从async await 报错Unexpected identifier 谈谈对上下文的理解

    原文首发地址: 先简单介绍下async await: async/await是ES6推出的异步处理方案,目的也很明确:更好的实现异步编程.   详细见阮大神 ES6入门 现在说说实践中遇到的问题:使用 ...

  9. STM32—无需中断来实现使用DMA接收串口数据

    本节目标: 通过DMA,无需中断,接收不定时长的串口数据 描述:当在串口多数据传输下,CPU会产生多次中断来接收串口数据,这样会大大地降低CPU效率,同时又需要CPU去做其它更重要的事情,我们应该如何 ...

  10. centos 7 && dotnet core 2.0 && nginx && supervisor

    前提 系统:centos 7 目录:/home/wwwroot/www.wuball.com dotnet core 2.0 官方指引 sudo rpm --import https://packag ...