在两道多线程基础题“顺序打印”中对比一下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 循环时,线程会在被唤醒后再次检查条件是否满足,如果不满足则继续等待,从而避免了虚假唤醒的问题。

相关文章
|
6天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
45 17
|
17天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
2天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
19天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
19天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
19天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
42 3
|
19天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
105 2
|
27天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
48 6
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
17天前
|
Java 调度