线程间通信

你好,我是吴计可师,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读


线程间通信是指多个线程之间如何交换数据、通知彼此的执行状态以及协调工作。线程间通信在并发编程中是一个重要的概念,它可以让多个线程共同工作,协调资源的访问,避免竞态条件和死锁。

01
共享变量(共享内存)


最简单的线程通信方式是通过共享内存(共享变量)来传递数据。多个线程可以访问同一个变量,通过读取和修改共享的变量来交换信息。常见的做法是通过同步来避免并发访问带来的数据不一致性。

02
wait() 和 notify() 方法


Java 中的 Object 类提供了 wait()notify() 方法,这两个方法是线程间通信的基础。wait() 用于让当前线程等待,直到其他线程调用 notify()notifyAll() 来唤醒它。notify() 用于唤醒一个正在等待该对象锁的线程,notifyAll() 用于唤醒所有等待该对象锁的线程。

示例:生产者-消费者问题

一个常见的例子是生产者和消费者问题,生产者将数据放入缓冲区,消费者从缓冲区取出数据。如果缓冲区为空,消费者需要等待;如果缓冲区满,生产者需要等待。

class SharedBuffer {    private int data = -1;    private boolean available = false;    public synchronized void produce(int value) throws InterruptedException {        while (available) {            wait();  // 如果有数据,生产者等待        }        data = value;        available = true;        System.out.println("Produced: " + value);        notify();  // 唤醒消费者    }    public synchronized int consume() throws InterruptedException {        while (!available) {            wait();  // 如果没有数据,消费者等待        }        available = false;        System.out.println("Consumed: " + data);        notify();  // 唤醒生产者        return data;    }}class Producer extends Thread {    private SharedBuffer buffer;    public Producer(SharedBuffer buffer) {        this.buffer = buffer;    }    @Override    public void run() {        try {            for (int i = 1; i <= 5; i++) {                buffer.produce(i);                Thread.sleep(500);            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}class Consumer extends Thread {    private SharedBuffer buffer;    public Consumer(SharedBuffer buffer) {        this.buffer = buffer;    }    @Override    public void run() {        try {            for (int i = 1; i <= 5; i++) {                buffer.consume();                Thread.sleep(1000);            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}public class ThreadCommunicationExample {    public static void main(String[] args) {        SharedBuffer buffer = new SharedBuffer();        Producer producer = new Producer(buffer);        Consumer consumer = new Consumer(buffer);        producer.start();        consumer.start();    }}

在这个例子中,生产者和消费者通过 wait()notify() 方法进行同步。生产者在生产时,如果数据已经存在,它会调用 wait() 使自己进入等待状态,直到消费者消费掉数据并唤醒它;消费者在消费时,如果没有数据,它会调用 wait(),直到生产者生产数据并唤醒它。


03
BlockingQueue(阻塞队列)


BlockingQueue 是 Java 提供的一种线程安全的队列,常用于生产者-消费者模型中。它实现了阻塞和非阻塞方法,线程在从空队列中取数据时会被阻塞,直到队列中有数据;线程在向满队列中放数据时,也会被阻塞,直到队列有空间。

常见的实现类有 ArrayBlockingQueueLinkedBlockingQueue


04
ExecutorService 和 Future


ExecutorService 是 Java 并发包中用于管理线程池的类,它可以通过 Future 对象来实现线程间的通信,Future 允许线程间共享计算结果。

示例:使用 ExecutorServiceFuture 进行线程间通信

import java.util.concurrent.*;class Task implements Callable<Integer> {    @Override    public Integer call() throws Exception {        System.out.println("Task started");        Thread.sleep(2000);  // 模拟耗时操作        return 42;    }}public class ThreadCommunicationExecutorExample {    public static void main(String[] args) throws InterruptedException, ExecutionException {        ExecutorService executor = Executors.newSingleThreadExecutor();        Future<Integer> future = executor.submit(new Task());        // 在此可以做其他工作        System.out.println("Doing other work...");        // 获取结果,阻塞直到任务完成        Integer result = future.get();        System.out.println("Task result: " + result);        executor.shutdown();    }}

在这个例子中,Future 提供了 get() 方法来获取异步任务的结果,get() 方法会阻塞,直到任务完成并返回结果。


05
直击面试


5.1 为什么需要线程间通信?

在多线程环境中,线程往往需要协调工作,避免冲突,同时保证共享资源的正确访问。

在这个例子中,Future 提供了 get() 方法来获取异步任务的结果,get() 方法会阻塞,直到任务完成并返回结果。

  • 一个线程需要等待另一个线程完成某项工作,才能继续执行。

  • 生产者-消费者问题等需要线程之间的协调。


5.2 Java 中有哪些常见的线程间通信方式?

wait() / notify() / notifyAll():这些方法是 Object 类的一部分,可以用于线程间的通信和同步。

BlockingQueue:如 ArrayBlockingQueue, LinkedBlockingQueue 等,这些是线程安全的队列,用于生产者-消费者模式。

CountDownLatch:用于等待某些线程完成后再继续执行。

CyclicBarrier:用于让一组线程等待彼此完成某项工作后再继续执行。

Semaphore:用于控制同时访问某些资源的线程数量。

Exchanger:允许两个线程交换对象。


5.3. CountDownLatch 和 CyclicBarrier 有什么区别?

CountDownLatch:它用于使一个或多个线程等待其他线程完成某项操作后再继续执行。适用于一次性的等待场景,比如等待多个线程执行完毕。它是不可重用的。

  • 示例:等待所有线程执行完毕后,主线程再执行。

CyclicBarrier:它也用于让多个线程在某个点上同步,并在所有线程到达该点后一起继续执行。与 CountDownLatch 不同,CyclicBarrier 是可重用的,适合循环使用。


5.4 如何使用 Exchanger 实现线程间的数据交换?

Exchanger 是一个用于线程间数据交换的同步工具类,它允许两个线程交换数据。每个线程提供一个数据对象,并在交换时交换这些对象。

import java.util.concurrent.*;class ExchangeTask implements Runnable {    private final Exchanger<String> exchanger;    public ExchangeTask(Exchanger<String> exchanger) {        this.exchanger = exchanger;    }    @Override    public void run() {        try {            String data = "Data from " + Thread.currentThread().getName();            String received = exchanger.exchange(data);  // 与另一个线程交换数据            System.out.println(Thread.currentThread().getName() + " received: " + received);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}public class ExchangerExample {    public static void main(String[] args) {        Exchanger<String> exchanger = new Exchanger<>();        Thread thread1 = new Thread(new ExchangeTask(exchanger));        Thread thread2 = new Thread(new ExchangeTask(exchanger));        thread1.start();        thread2.start();    }}

今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海

喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成


一个阿里二面面试官必问的问题


谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


分享面试:mysql数据库索引失效的情况


面试常被忽略的问题——内存区域划分