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

简介: 多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同。1. volatile、synchronized关键字PS:关于volatile的详细介绍请移步至:Java并发编程的艺术(三)——volatile1.1 如何实现通信?这两种方式都采用了同步机制实现多条线程间的数据通信。

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

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

}
相关文章
|
5天前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
14天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
34 2
|
2天前
|
Java API 调度
Java 多线程编程详解
《Java多线程编程详解》深入浅出地讲解了Java平台下的多线程核心概念、API使用及最佳实践。从基础理论到实战案例,本书帮助读者掌握并发编程技巧,提升软件开发中的效率与性能,是Java开发者不可或缺的参考指南。
|
2天前
|
Oracle Java 关系型数据库
Java编程之旅:从基础到进阶
Java,一种广泛使用的编程语言,因其平台无关性、面向对象的特性而备受推崇。本文旨在通过简明易懂的语言和实际代码示例,引导初学者了解Java的基本概念,并逐步深入到更复杂的编程技巧。我们将从Java的安装开始,经过变量、数据类型、控制结构等基础知识的学习,最后探讨异常处理和文件操作等进阶话题。无论你是编程新手还是有一定经验的开发者,这篇文章都将为你提供有价值的参考和启示。
|
1天前
|
安全 Java 调度
理解 Java 中的多线程编程
本文深入探讨了Java中的多线程编程,涵盖线程创建与管理、同步机制、锁及死锁避免策略。介绍了通过继承`Thread`类或实现`Runnable`接口创建线程的方法,并讨论了线程的生命周期状态。此外,还讲解了如何使用`ExecutorService`线程池以及`java.util.concurrent`包中的工具类来简化并发编程。理解这些概念和技术,有助于开发高效稳定的多线程应用程序。
|
2天前
|
Java
用java实现Client和Server之间的互相通信
本文介绍了如何使用Java实现客户端和服务器之间的通信,包括服务器端创建ServerSocket、接受客户端连接、读取和发送消息,以及客户端创建Socket连接、发送和接收消息的完整过程。
9 0
用java实现Client和Server之间的互相通信
|
3天前
|
缓存 安全 Java
三万字长文Java面试题——基础篇(注:该篇博客将会一直维护 最新维护时间:2024年9月18日)
本文是一篇全面的Java面试题指南,涵盖了Java基础、数据类型、面向对象、异常处理、IO流、反射、代理模式、泛型、枚举、Lambda表达式、Stream流等多个方面的知识点,并提供了详细的解析和代码示例。
15 0
三万字长文Java面试题——基础篇(注:该篇博客将会一直维护 最新维护时间:2024年9月18日)
|
1天前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
了解final关键字在Java并发编程领域的作用吗?
|
3天前
|
缓存 安全 Java
Singleton:在 Java 编程中编写和使用的 6 种方法
Singleton:在 Java 编程中编写和使用的 6 种方法
8 0
|
3天前
|
安全 Java 编译器
了解final关键字在Java并发编程领域的作用吗?
【10月更文挑战第8天】在Java并发编程中,`final`关键字具有重要作用,包括保证变量的可见性和不可变性,防止对象引用被意外修改,并帮助编译器优化读取操作及消除不必要的同步。通过确保变量不可变,`final`增强了多线程环境下的安全性与性能。