多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同。

1. volatile、synchronized关键字

PS:关于volatile的详细介绍请移步至:Java并发编程的艺术(三)——volatile

1.1 如何实现通信?

这两种方式都采用了同步机制实现多条线程间的数据通信。与其说是“通信”,倒不如说是“共享变量”来的恰当。当一个共享变量被volatile修饰 或 被同步块包裹后,他们的读写操作都会直接操作共享内存,从而各个线程都能看到共享变量最新的值,也就是实现了内存的可见性。

1.2 特点

  • 这种方式本质上是“共享数据”,而非“传递数据”;只是从结果来看,数据好像是从写线程传递到了读线程;
  • 这种通信方式无法指定特定的接收线程。当数据被修改后究竟哪条线程最先访问到,这由操作系统随机决定。
  • 总的来说,这种方式并不是真正意义上的“通信”,而是“共享”。

1.3 使用场景

这种方式能“传递”变量。当需要传递一些公用的变量时就可以使用这种方式。如:传递boolean flag,用于表示状态、传递一个存储所有任务的队列等。

1.4 例子

用这种方式实现线程的开关控制。

// 用于控制线程当前的执行状态
private volatile boolean running = false;// 开启一条线程
Thread thread = new Thread(new Runnable(){void run(){// 开关while(!running){Thread.sleep(1000);}// 执行线程任务doSometing();}
}).start();// 开始执行
public void start(){running = true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2. 等待/通知机制

2.1 如何实现?

等待/通知机制的实现由Java完成,我们只需调用Object类的几个方法即可。

  • wait():将当前线程的状态改为“等待态”,加入等待队列,释放锁;直到当前线程发生中断或调用了notify方法,这条线程才会被从等待队列转移到同步队列,此时可以开始竞争锁。
  • wait(long):和wait()功能一样,只不过多了个超时动作。一旦超时,就会继续执行wait之后的代码,它不会抛超时异常!
  • notify():将等待队列中的一条线程转移到同步队列中去。
  • notifyAll():将等待队列中的所有线程都转移到同步队列中去。

2.2 注意点

  • 以上方法都必须放在同步块中;
  • 并且以上方法都只能由所处同步块的锁对象调用;
  • 锁对象A.notify()/notifyAll()只能唤醒由锁对象A wait的线程;
  • 调用notify/notifyAll函数后仅仅是将线程从等待队列转移到阻塞队列,只有当该线程竞争到锁后,才能从wait方法中返回,继续执行接下来的代码;

2.3 QA

  • 为什么wait必须放在同步块中调用? 
    因为等待/通知机制需要和共享状态变量配合使用,一般是先检查状态,若状态为true则执行wait,即包含“先检查后执行”,因此需要把这一过程加锁,确保其原子执行。 
    举个例子:
// 共享的状态变量
boolean flag = false;// 线程1
Thread t1 = new Thread(new Runnable(){public void run(){while(!flag){wait();}}
}).start();// 线程2
Thread t2 = new Thread(new Runnable(){public void run(){flag = true;notifyAll();}
}).start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上述例子thread1未加同步。当thread1执行到while那行后,判断其状态为true,此时若发生上下文切换,线程2开始执行,并一口气执行完了;此时flag已经是true,然而thread1继续执行,遇到wait后便进入等待态;但此时已经没有线程能唤醒它了,因此就一直等待下去。

  • 为什么notify需要加锁?且必须和wait使用同一把锁? 
    首先,加锁是为了保证共享变量的内存可见性,让它发生修改后能直接写入共享内存,好让wait所处的线程立即看见。 
    其次,和wait使用同一把锁是为了确保wait、notify之间的互斥,即:同一时刻,只能有其中一条线程执行。

  • 为什么必须使用同步块的锁对象调用wait函数? 
    首先,由于wait会释放锁,因此通过锁对象调用wait就是告诉wait释放哪个锁。 
    其次,告诉线程,你是在哪个锁对象上等待的,只有当该锁对象调用notify时你才能被唤醒。

  • 为什么必须使用同步块的锁对象调用notify函数? 
    告诉notify,只唤醒在该锁对象上等待的线程。

2.4 代码实现

等待/通知机制用于实现生产者和消费者模式。

  • 生产者
synchronized(锁A){flag = true;// 或者:list.add(xx);锁A.notify();
}
  • 1
  • 2
  • 3
  • 4
  • 消费者
synchronized(锁A){// 不满足条件while(!flag){ // 或者:list.isEmpty()锁A.wait();}// doSometing……
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.5 超时等待模式

在之前的生产者-消费者模式中,如果生产者没有发出通知,那么消费者将永远等待下去。为了避免这种情况,我们可以给消费者增加超时等待功能。该功能依托于wait(long)方法,只需在wait前的检查条件中增加超时标识位,实现如下:

public void get(long mills){synchronized( list ){// 不加超时功能if ( mills <= 0 ) {while( list.isEmpty() ){list.wait();}}// 添加超时功能else {boolean isTimeout = false;while(list.isEmpty() && isTimeout){list.wait(mills);isTimeout = true;}// doSometing……}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3. 管道流

3.1 作用

管道流用于在两个线程之间进行字节流或字符流的传递。

3.2 特点

  • 管道流的实现依靠PipedOutputStream、PipedInputStream、PipedWriter、PipedReader。分别对应字节流和字符流。
  • 他们与IO流的区别是:IO流是在硬盘、内存、Socket之间流动,而管道流仅在内存中的两条线程间流动。

3.3 实现

步骤如下: 
1. 在一条线程中分别创建输入流和输出流; 
2. 将输入流和输出流连接起来; 
3. 将输入流和输出流分别传递给两条线程; 
4. 调用read和write方法就可以实现线程间通信。

// 创建输入流与输出流对象
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();// 连接输入输出流
out.connect(in);// 创建写线程
class WriteThread extends Thread{private PipedWriter out;public WriteThread(PipedWriter out){this.out = out;}public void run(){out.write("hello concurrent world!");}
}// 创建读线程
class ReaderThread extends Thread{private PipedReader in;public ReaderThread(PipedReader in){this.in = in;}public void run(){in.read();}
}// 
  • 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

4. join

4.1 作用

  • join能将并发执行的多条线程串行执行;
  • join函数属于Thread类,通过一个thread对象调用。当在线程B中执行threadA.join()时,线程B将会被阻塞(底层调用wait方法),等到threadA线程运行结束后才会返回join方法。
  • 被等待的那条线程可能会执行很长时间,因此join函数会抛出InterruptedException。当调用threadA.interrupt()后,join函数就会抛出该异常。

4.2 实现

public static void main(String[] args){// 开启一条线程Thread t = new Thread(new Runnable(){public void run(){// doSometing}}).start();// 调用join,等待t线程执行完毕try{t.join();}catch(InterruptedException e){// 中断处理……}}

java面试-Java并发编程(六)——线程间的通信相关推荐

  1. 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

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

  2. java判断线程是否wait_Java并发编程之线程间通讯(上)wait/notify机制

    线程间通信 如果一个线程从头到尾执行完也不和别的线程打交道的话,那就不会有各种安全性问题了.但是协作越来越成为社会发展的大势,一个大任务拆成若干个小任务之后,各个小任务之间可能也需要相互协作最终才能执 ...

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

    点击上方"Coder编程",选择"置顶公众号" 技术文章第一时间送达! 并发编程.png 每天进步一点,不做curd工程师与Api调用工程师 欢迎访问 个人博客 ...

  4. 【Java面试】并发编程实战(线程控制操作详解)

     个人简介

  5. java task和thread_【Java学习笔记-并发编程】线程与任务

    前言 最近在看一些Java15的并发.线程调度以及一些实现方案的东西,虽然很多东西还是 1.5 的,但还是很有收获. 一.线程与任务 Java中,要用线程来执行任务,线程可以说是任务的容器.没有线程的 ...

  6. 【Java 并发编程】线程简介 ( 进程与线程 | 并发概念 | 线程间通信 | Java 并发 3 特性 )

    文章目录 一.进程与线程 二.并发 三.线程间通信 四.Java 并发 3 特性 一.进程与线程 最开始是没有线程这个概念的 , 一个应用程序就是一个进程 , 应用程序运行时 , 如果还要处理与用户交 ...

  7. Java并发基础(六) - 线程池

    Java并发基础(六) - 线程池 1. 概述 这里讲一下Java并发编程的线程池的原理及其实现 2. 线程池的基本用法 2.1 线程池的处理流程图 该图来自<Java并发编程的艺术>: ...

  8. (转)Java并发编程:线程池的使用

    背景:线程池在面试时候经常遇到,反复出现的问题就是理解不深入,不能做到游刃有余.所以这篇博客是要深入总结线程池的使用. ThreadPoolExecutor的继承关系 线程池的原理 1.线程池状态(4 ...

  9. 【Java 并发编程】线程池机制 ( ThreadPoolExecutor 线程池构造参数分析 | 核心线程数 | 最大线程数 | 非核心线程存活时间 | 任务阻塞队列 )

    文章目录 前言 一.ThreadPoolExecutor 构造参数 二.newCachedThreadPool 参数分析 三.newFixedThreadPool 参数分析 四.newSingleTh ...

最新文章

  1. 跟我学Springboot开发后端管理系统4:数据库连接池Druid和HikariCP
  2. 程序员的爱情 第六章
  3. android如何查看分区信息,android如何查看分区信息
  4. 陶晶驰stm32_陶晶驰串口屏学习日记(1)
  5. ajax nginx 转发 sessionid_「查缺补漏」巩固你的Nginx知识体系
  6. 试用到期_各大化妆品品牌试用装广告
  7. 关于C++中的 多态 问题
  8. Tarjan算法——强连通分量
  9. 视频人像磨皮插件:Beauty Box 4.2
  10. 51单片机汇编程序,温度报警项目
  11. wdr7660虚拟服务器设置,TP-LINK WDR7660用手机怎么设置?
  12. C语言的进制转换以及算法实现
  13. java 策略模式会员_设计模式——策略模式:会员价格体系的简单实现
  14. android 支付宝私钥加密,支付宝支付密钥RSA1升级到RSA2
  15. 官宣,北京杜绝现场复试!清华等全国多地高校确定将网络远程复试
  16. Arch Linux 安装后无法联网的问题
  17. linux 实验心得体会
  18. macOS Chrome无法访问自签名https页面问题的解决办法
  19. 李飞飞CS231n2017课程双语字幕版上线 !(附课程链接)
  20. UiPath Robotic Enterprise Framework 学习笔记

热门文章

  1. ie6 ie7下使用clear不能将浮动的元素换行问题
  2. Thinking In Design Pattern——Query Object模式
  3. SWT多线程注意事项
  4. Linux下tomcat的配置
  5. wex5链接mysql_wex5数据库连接自己的数据库在哪里配置的
  6. python实现守护进程_守护进程原理及Python实现
  7. Golang——深浅拷贝
  8. c盘java文件误删_C盘爆满怎么办,教你有选择性删除文件,恢复空间
  9. android系统源码的环境下用make来编译,Android源码编译系统入门
  10. php switch if,php switch 与 if else 区别