Java并发编程的艺术(六)——线程间的通信

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

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){
        // 中断处理……
    }

}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏闻道于事

Java多线程详解

每个运行的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个进程。

683
来自专栏python3

python3--线程,锁,同步锁,递归锁,信号量,事件,条件和定时器,队列,线程池

这个99又赋值给n,进程变量就是99,所以每次都是赋值操作,赋值了100次,最终结果99,这样还是出现数据不安全的情况

1232
来自专栏Janti

JAVA多线程高并发学习笔记(三)——Callable、Future和FutureTask

为什么要是用Callable和Future Runnable的局限性 Executor采用Runnable作为基本的表达形式,虽然Runnable的run方法能...

3115
来自专栏IT技术精选文摘

Java多线程知识小抄集(一)

本文主要整理平时遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子...

1889
来自专栏Java后端技术

使用Java客户端对Redis进行操作

  上篇文章我们介绍了如何在centos7下面进行安装单机版redis以及redis集群。这篇文章,我们来聊一聊如何使用java客户端来进行操作redis。我们...

472
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十四天 多线程-多线程&线程安全【悟空教程】

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

975
来自专栏chenssy

【死磕Java并发】-----J.U.C之线程池:线程池的基础架构

原文出处http://cmsblogs.com/ 『chenssy』 经历了Java内存模型、JUC基础之AQS、CAS、Lock、并发工具类、并发容器、阻塞队...

3115
来自专栏kevindroid

java多线程——线程的状态

1303
来自专栏coder修行路

Python 并发编程(一)之线程

常用用法 t.is_alive() Python中线程会在一个单独的系统级别线程中执行(比如一个POSIX线程或者一个Windows线程) 这些线程将由操作系统...

1756
来自专栏栗霖积跬步之旅

java多线程编程核心技术——第三章总结

第一节等待/通知机制 1.1不使用等待/通知机制实现线程间的通讯 1.2什么是等待/通知机制 1.3等待/通知机制的实现 1.4方法wait()锁释放与noti...

19810

扫码关注云+社区