在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()

简介: 这篇内容讨论了如何在Java中通过多线程控制特定顺序的打印任务。

一、基础


有三个线程,线程名称分别为:a,b,c,每个线程打印自己的名称。


需要让他们同时启动,并按 c,b,a的顺序打印。


这道题要求打印 cba,且只打印一次。如何保证线程 cba 的执行顺序?容易想到,只需要让这三个线程按一定顺序串行执行即可,采用 join() 就可以轻易做到。


join() 的作用是,让当前线程等待调用 join() 的线程执行完毕后,再继续往下执行。在使用 `join()` 方法时,调用线程会进入等待状态,直到被等待的线程执行完毕。在 Java 中,可以通过 `join()` 方法来实现线程之间的同步。例如,在一个多线程程序中,如果需要让线程 A 在线程 B 执行完后再继续执行,可以在线程 A 中调用线程 B 的 `join()` 方法。这样,线程 A 会等待线程 B 终止后再继续执行。


public class Test {
    public static void main(String[] args) throws InterruptedException {
 
        Thread t1 = new Thread(() -> {
            System.out.print("A");
        });
 
        Thread t2 = new Thread(() -> {
            System.out.print("B");
        });
 
        Thread t3 = new Thread(() -> {
            System.out.print("C");
        });
 
        t3.start();
        t3.join();
        t2.start();
        t2.join();
        t1.start();
        t1.join();
    }
}



二、进阶


有三个线程,分别只能打印A,B和C。要求按顺序打印ABC,打印10次。


输出示例:


ABC


ABC


ABC


ABC


ABC


ABC


ABC


ABC


ABC


ABC


这道题的特点在循环打印。循环打印的情况下,join方法就不那么适用了。因为在这个场景中,线程 A、B、C 需要循环执行多次,如果在每次执行结束后都使用 `join()` 方法等待另外两个线程执行完毕后再继续执行,会导致线程的阻塞和唤醒操作频繁地发生,从而影响程序的性能。


况且,实现起来也并不那么直接。以下的几种做法都是不正确的:



错误方法-1:该代码实际上只能打印出一次ABC。因为join是等待线程完全终止。虽然有多次循环,但实际上只有第一次循环执行时,启动了线程;后面线程终止后,就没有再启动过了



错误方法-2:会报线程状态异常。因为在同一线程中,反复多次start()了同一线程。


相比之下,在这个场景中,使用 wait() 和 notifyAll() 方法可以更好地实现线程之间的同步和协作。


使用 wait() 和 notifyAll() 方法可以让线程在需要等待的时候进入等待状态,直到满足某个条件后再唤醒线程。


思路如下:


创建了一个 PrintABC 类,其中包含了打印A、B、C的三个方法,以及控制打印顺序的状态值 count 和用于线程间通信的锁对象 locker。


在每个打印方法中,使用了一个 while 循环来判断是否轮到该线程打印。为什么使用while而不是if,这点后面再说。该程序中,如果不是自己打印的轮次,则调用 wait() 方法使线程等待,否则进行打印操作。


在打印完成后,将count加1,并调用 notifyAll() 方法通知其他线程。也即,每次有一个线程执行了打印过之后,就要把所有线程都唤醒,让它们再判断一次是否轮到自己打印了。


最后,在 main() 方法中创建三个线程并启动它们,分别调用打印A、B、C的方法。执行程序后,即可按顺序打印10次ABC。


class PrintABC {
    static final Object locker = new Object();    // 锁对象
    static int count;    // 状态值,用于控制打印顺序
 
 
    public static void printA() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 0) {    // 判断是否轮到该线程打印
                    locker.wait();
                }
 
                count++;    // 状态值加1,并通知其他线程
                System.out.print("A");
                locker.notifyAll();
            }
        }
    }
    public static void printB() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 1) {
                    locker.wait();
                }
 
                count++;
                System.out.print("B");
                locker.notifyAll();
            }
        }
    }
    public static void printC() throws InterruptedException {
        synchronized (locker) {
            for (int i = 0; i < 10; i++) {
                while(count % 3 != 2) {
                    locker.wait();
                }
 
                count++;
                System.out.println("C");
                locker.notifyAll();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
 
        Thread t1 = new Thread(() -> {
            try {
                PrintABC.printA();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
 
        Thread t2 = new Thread(() -> {
            try {
                PrintABC.printB();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
 
        Thread t3 = new Thread(() -> {
            try {
                PrintABC.printC();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
 
 
        t1.start();
        t2.start();
        t3.start();
    }
}


Q:为什么要notifyAll,而不是notify?


A:在上面的Java代码示例中,我们使用 notifyAll() 方法来通知其他线程,而不是使用 notify() 方法。这是因为在有多个线程正在 wait 时, notify() 方法只会随机唤醒一个等待该对象锁的线程,而 notifyAll() 方法会唤醒所有等待该对象锁的线程。由于我们希望所有等待线程都能被唤醒并进行状态判断,因此使用 notifyAll() 更为合适。


Q:为什么要用while(count % 3 != 0)而不是if(count % 3 != 0)?


A:在多线程编程中,使用 while 循环来判断条件是否满足,通常是为了避免虚假唤醒的问题。


什么是虚假唤醒?贴一个大佬的总结:Java线程虚假唤醒是什么、如何避免?


多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们;假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,是虚假唤醒。


换句话说,虽然notifyAll()把所有线程都喊醒了,但该程序中要求最终只有一个线程是能继续执行的;其它线程还得wait();那么醒来的这个“其它线程”,就是虚假唤醒。


在本例中,我们希望三个线程分别打印字母 A、B、C,且每个线程只能打印一种字母。为了实现这个目标,我们引入了一个状态变量 count,表示当前可以打印的字母是哪个线程负责打印的。具体来说,当 count 的值为 0、1、2 时,分别表示线程 A、B、C 可以打印字母;当 count 的值为 3、4、5 时,表示线程 A、B、C 分别已经打印完了一次字母,需要等待其他线程打印完后才能再次打印。


比如说,此时count是1。那么一开始,三个线程其实都会被唤醒,然后判断当前自己应不应该打印字母。但是此时只有B是可以被打印的,A和C就是虚假唤醒。如果是if,那if进行了一次判断后,A和C依旧进入wait。然而,等到B执行完后,再次notifyAll,A和C在醒来之后不会再有第二次条件判断,而是直接在wait这行代码处被唤醒并接着向下执行了,A和C会同时执行打印操作。这样就会打乱打印顺序。




if-运行结果


为了避免这种情况,我们使用 while 循环来判断条件是否满足。在使用 while 循环时,线程会在被唤醒后再次检查条件是否满足,如果不满足则继续等待,从而避免了虚假唤醒的问题。

相关文章
|
1月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
109 1
|
1月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
116 1
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
116 0
|
2月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
181 16
|
3月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
3月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
4月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
337 83
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
177 0
|
4月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
318 83