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

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;
}

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();

上述例子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();
}
  • 消费者
synchronized(锁A){
// 不满足条件
while(!flag){ // 或者:list.isEmpty()
锁A.wait();
} // doSometing……
}

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……
}
}
}

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();
}
} //

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并发编程的艺术(六)——线程间的通信的更多相关文章

  1. java并发编程(十一)线程间的通信notify通知的遗漏

    notify通知的遗漏很容易理解,即threadA还没开始wait的时候,threadB已经notify了,这样,threadB通知是没有任何响应的,当threadB退出synchronized代码块 ...

  2. Java并发编程(八)线程间协作(上)

    多线程并发执行时,不同的线程执行的内容之间可能存在一些依赖关系,比如线程一执行a()方法和c()方法,线程二执行b()方法,方法a()必须在方法b()之前执行,而方法c()必须在方法b()之后执行.这 ...

  3. Java并发编程(十三)线程间协作的两种方式:wait、notify、notifyAll和Condition

    在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果 ...

  4. Java并发编程(九)线程间协作(下)

    上篇我们讲了使用wait()和notify()使线程间实现合作,这种方式很直接也很灵活,但是使用之前需要获取对象的锁,notify()调用的次数如果小于等待线程的数量就会导致有的线程会一直等待下去.这 ...

  5. Java并发编程的艺术(十一)——线程池(2)

    Executor两级调度模型 在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程. 在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执 ...

  6. Java并发编程的艺术(四)——线程的状态

    线程的状态 初始态:NEW 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态. 运行态:RUNNABLE 在Java中,运行态包括就绪态 和 运行态. 就绪态 该状态下的线 ...

  7. Java并发编程的艺术(十)——线程池(1)

    线程池的作用 减少资源的开销 减少了每次创建线程.销毁线程的开销. 提高响应速度 每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度. 提高线程的可管理性 线程是一种稀缺资 ...

  8. Java并发编程的艺术(十)——线程池

    线程池的作用 降低资源消耗.重复利用已有线程,减少线程的创建和销毁造成的消耗. 提高响应速度.当有任务需要处理的时候,就不用再花费重新创建线程的时间了. 提高线程的可管理性.不合理利用线程,会浪费资源 ...

  9. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

随机推荐

  1. CAS5.2x单点登录(一)——搭建cas服务器

    系列文章列表: https://blog.csdn.net/u013825231/article/category/7517313 单点登录的介绍 单点登录(Single Sign On ,简称SSO ...

  2. sklearn GMM模型介绍

    参考  SKlearn 库 EM 算法混合高斯模型参数说明及代码实现   和   sklearn.mixture.GaussianMixture 以前的推导内容:    GMM 与 EM 算法 记录下 ...

  3. 【LOJ】#2128. 「HAOI2015」数字串拆分

    题解 题中给的函数可以用矩阵快速幂递推 我们记一个数组dp[i](这个数组每个元素是一个矩阵)表示从1到i所有的数字经过拆分矩阵递推的加和 转移方法是 \(dp[i] = \sum_{j = 0}^{ ...

  4. 【noip模拟赛3】贾老二的工件 (模拟)

    描述 贾老二有很多工件,最常见的工件都是长条形的,但其顶端是凹凸不平的,即不同位置的高度不同.现在贾老二有两个最常见的工件,他想将它们完全放入另一种罕见的可容纳高度不超过k的工件中,问该罕见的工件的最 ...

  5. 【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))

    本文根据<大话数据结构>一书,实现了Java版的一个简单的散列表(哈希表). 基本概念 对关键字key,将其值存放在f(key)的存储位置上.由此,在查找时不需比较,只需计算出f(key) ...

  6. 动态SQL与模糊查询

    一: 1.需求 实现多条件查询用户(姓名模糊查询,年龄在指定的最小值与最大值之间) 2.结构目录 3.准备数据与建表 CREATE TABLE d_user( id int PRIMARY KEY A ...

  7. 026 Spark 的官网(版本为1.6.1的总官网)

    1. 多多读官网,所有的只知识点都可以从上面的总纲中查到.

  8. python 函数式编程学习笔记

    函数基础 一个函数就是将一些语句集合在一起的部件,它们能够不止一次地在程序中运行.函数的主要作用: 最大化的代码重用和最小化代码冗余 流程的分解 一般地,函数讲的流程是:告诉你怎样去做某事,而不是让你 ...

  9. View初探

    View初探 学习自 <Android开发艺术探索> View漫谈 Activity构成了我们的界面但是知识一个空壳子,Activity与View相结合才构成了我们丰富多彩的界面,并且为了 ...

  10. 任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。

    任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和. 当n=7共14种拆分方法: 7=1+1+1+1+1+1+1 7=1+1+1+1+1+2 7=1+1+1+1+3 7=1+1+1+2+2 ...