掘金 后端 ( ) • 2024-04-09 09:31

theme: orange highlight: a11y-dark

咦咦咦,各位小可爱,我是你们的好伙伴 bug菌,今天又来给大家手把手教学Java SE系列知识点啦,赶紧出来哇,别躲起来啊,听我讲干货记得点点赞,赞多了我就更有动力讲得更欢哦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

日常开发中,少不了多线程相关编写,对于线程间的通信,它就是一个重要的概念。线程通信能够确保了多个线程可以协同工作,共享数据,并在必要时相互等待或通知。而今天,我就是要给大家介绍Java中线程通信的基本概念、方法和应用场景,旨在帮助Java小白理解和掌握线程通信的技巧。

摘要

首先对于线程通信,它允许不同线程之间进行数据的交换和同步。在Java中,线程通信可以通过共享对象的方式来实现,通过使用共享对象的waitnotifynotifyAll方法,线程之间可以实现等待唤醒的操作,从而实现线程之间的协调与合作。

概述

在多线程编程中,线程之间的通信是非常常见的场景之一了。例如,一个线程可能需要等待另一个线程完成某个操作后才能继续执行,或者多个线程之间需要共享某个变量。线程通信可以有效地解决这些问题,使得多线程编程更加灵活和高效,那具体如何解决呢?

看这里,Java提供了几种机制来实现线程通信,其中最常用的是使用共享对象的waitnotifynotifyAll方法。wait方法使得当前线程进行等待,直到其他线程调用该对象的notifynotifyAll方法才能将它唤醒;notify 方法作用是用于唤醒等待在该对象上的一个线程;而notifyAll方法则是用于唤醒等待在该对象上的所有线程,大白话就是唤醒单复数的意思。

Java线程通信

源代码解析

这里,开局来段示例代码(so easy!),给大家演示下线程通信的基本用法,同学们瞧好了:

我们先来定义一个启动类,展示如何在Java中创建和管理线程,以及如何通过共享资源来实现线程间的协作。

/**
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024-04-03 18:37
 */
public class ThreadCommunicationExample {
    public static void main(String[] args) {
        //模拟了一个生产者-消费者场景
        final Processor processor = new Processor();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    processor.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    processor.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

其次,这里我们再来定义一个模拟生产者-消费者场景的类,然后分别定义一个生产者类跟消费者类。

/**
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024-04-03 18:37
 */
public class Processor {
    
    public void produce() throws InterruptedException {
        synchronized (this) {
            System.out.println("Producer thread running...");
            wait();
            System.out.println("Producer thread resumed...");
        }
    }

    public void consume() throws InterruptedException {
        Thread.sleep(2000);

        synchronized (this) {
            System.out.println("Consumer thread running...");
            notify();
            Thread.sleep(5000);
        }
    }
}

在这个示例中,有两个线程:一个生产者线程和一个消费者线程。生产者线程在synchronized块中调用了wait方法,进入等待状态;而消费者线程在synchronized块中调用了notify方法,唤醒了生产者线程。

实际执行结果展示如下:

应用场景案例

然后经过上述简单演示,这里再给大家介绍下线程通信在实际开发中的一些应用场景。以下是一些常见的应用场景案例:

  1. 生产者-消费者模型:多个生产者线程和多个消费者线程之间通过共享对象进行数据交换,实现生产者和消费者的协作。
  2. 售票系统:多个售票窗口同时售卖车票,通过线程通信来避免多个窗口同时售卖同一张车票。
  3. 线程池:线程池中的多个工作线程之间需要协调工作,通过线程通信来实现任务的分配和执行。
  4. 消息队列:多个生产者线程将消息放入队列,多个消费者线程从队列中取出消息,通过线程通信来实现生产者和消费者之间的交互。

优缺点分析

线程通信的优点是可以使多个线程之间能够协调工作,实现数据的交换和同步。它能够提高程序的并发性和效率,提高系统的吞吐量。

然而,任何事物都有优劣性;对于线程通信也有一些缺点。首先,使用线程通信机制会增加程序的复杂性,需要仔细设计和实现。其次,线程通信可能会引发一些常见的并发问题,如死锁和竞态条件。因此,在使用线程通信时需要注意相关的并发问题,并进行适当的同步和锁定操作。

类代码方法介绍

Processor类

produce方法

public void produce() throws InterruptedException {
    synchronized (this) {
        System.out.println("Producer thread running...");
        wait();
        System.out.println("Producer thread resumed...");
    }
}

produce()方法是一个生产者方法,通过synchronized关键字实现对共享对象的互斥访问。在方法内部使用wait方法使得当前线程进入等待状态,并释放对该对象的锁定。当其他线程调用该对象的notify或notifyAll方法时,该线程会被唤醒并继续执行。

consume方法

public void consume() throws InterruptedException {
    Thread.sleep(2000);

    synchronized (this) {
        System.out.println("Consumer thread running...");
        notify();
        Thread.sleep(5000);
    }
}

consume方法是一个消费者方法,通过synchronized关键字实现对共享对象的互斥访问。在方法内部使用notify方法唤醒等待在该对象上的一个线程。注意,notify方法不会释放对该对象的锁定,直到执行完synchronized块才会释放。

测试用例

测试代码

如下我给大家简单演示下如何验证线程通信的功能,如下是一个简单的测试用例(实则不简单),仅供参考:

/**
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2024-04-03 18:52
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个有界阻塞队列,容量为10
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
        // 创建一个倒计时锁存器,用于生产者和消费者线程的同步
        CountDownLatch latch = new CountDownLatch(2); // 两个线程
        // 创建一个信号量,用于控制生产者和消费者的并发访问
        Semaphore semaphore = new Semaphore(1, true);

        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    // 生产数据并放入队列
                    queue.put("Product " + (i + 1));
                    // 通知消费者可以消费
                    semaphore.release();
                    // 等待消费者消费
                    semaphore.acquire();
                    // 减少倒计时
                    latch.countDown(); 
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    // 从队列中取出数据
                    String product = queue.take();
                    System.out.println("Consumed: " + product);
                    // 通知生产者可以生产
                    semaphore.release();
                    // 减少倒计时
                    latch.countDown(); 
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 启动生产者和消费者线程
        producer.start();
        consumer.start();

        // 等待倒计时锁存器归零,表示所有线程都已完成它们的任务
        latch.await();

        System.out.println("All tasks completed. Exiting program.");
    }
}

在这个测试用例中,创建了一个Processor对象,并创建了一个生产者线程和一个消费者线程来测试线程通信的功能。

测试结果

根据如上测试代码,本地执行结果展示如下:仅供参考

测试代码解析

如上测试用例演示了一个简单的生产者-消费者问题,使用了ArrayBlockingQueueCountDownLatchSemaphore来进行线程间的通信和同步。以下是对代码的详细分析,希望能够帮助到同学们。

成员变量

  • ArrayBlockingQueue<String> queue: 定义的是一个有界阻塞队列,用于存储字符串类型的数据。队列的容量被初始化为10。当队列为空时,消费者从队列中取出数据会阻塞,而当队列已满时,生产者尝试放入数据也会阻塞。
  • CountDownLatch latch: 定义的是一个倒计时锁存器,用于同步两个线程(生产者和消费者)。它被初始化为2,意味着有两个线程将到达最后的latch.countDown()调用。
  • Semaphore semaphore: 这是一个信号量,用于控制生产者和消费者对共享资源(队列)的访问。它被初始化为1,表示同时只允许一个线程访问队列。

线程定义

  • Thread producer: 生产者线程,定义它的意义在于负责生成数据并将其放入队列。它循环5次,每次生成一个数据项,并通过queue.put()方法放入队列。然后,它释放一个信号量,允许消费者进行消费,并等待消费者完成消费后再继续生产。
  • Thread consumer: 消费者线程,负责从队列中取出数据并消费。它在一个无限循环中,通过queue.take()方法从队列中取出数据,并打印消费信息。消费完成后,它释放一个信号量,允许生产者生成新的数据。

线程启动和同步

  • 两个线程producerconsumer被启动后,将并发执行。
  • latch.await()使主线程等待,直到latch的计数器归零。这意味着主线程将等待生产者和消费者线程完成它们的任务。

异常处理

在生产者和消费者的循环中,InterruptedException被捕获并处理。如果线程在等待或阻塞操作中被中断,异常将被抛出,并打印堆栈跟踪。

程序结束

latch.await()返回时,意味着所有线程都已完成它们的任务。此时,主线程打印一条消息并退出程序。

潜在问题

其实上述测试用例呢,写的不够严谨,希望同学们只是单纯看着演示即可,实际上是需要留意的,这里我主要是为了演示,就没有写的够严谨,有兴趣的同学可以自行去修缮下。

  • 消费者线程是一个无限循环,它将永远运行,直到程序被外部停止。在实际应用中,你需要一种方法来优雅地关闭消费者线程。
  • 生产者线程在生成5个产品后结束,但消费者线程将继续运行。你可能需要一个条件或信号来控制消费者线程的生命周期。

测试小结

测试代码主要是给大家演示如何使用Java并发工具来解决生产者-消费者问题;通过使用阻塞队列、倒计时锁存器和信号量,程序实现了线程间的同步和通信。然而,为了使程序在实际应用中更加健壮和灵活,需要进一步考虑线程的生命周期管理和退出条件。

全文小结

本文以Java开发语言为例,内容上主要介绍了线程通信的概念、应用场景、源代码解析、优缺点分析以及类代码方法。通过学习和了解线程通信的相关知识,可以帮助开发者更好地设计和实现多线程并发程序。

总结

线程通信是多线程编程中一个重要的概念,通过线程通信可以实现线程之间的协调与合作。Java提供了几种机制来实现线程通信,其中最常用的是使用共享对象的wait、notify和notifyAll方法。在使用线程通信时需要注意相关的并发问题,并进行适当的同步和锁定操作。

... ...

ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,每天学习一小节,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!

「赠人玫瑰,手留余香」,咱们下期拜拜~~

附录源码

如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。

☀️建议/推荐你

无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。

码字不易,如果这篇文章对你有所帮助,帮忙给bugj菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。

📣关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 20w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!