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

相关文章
|
5天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
18 2
|
9天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
10天前
|
Java 开发者
Java中的多线程基础与应用
【9月更文挑战第22天】在Java的世界中,多线程是一块基石,它支撑着现代并发编程的大厦。本文将深入浅出地介绍Java中多线程的基本概念、创建方法以及常见的应用场景,帮助读者理解并掌握这一核心技术。
|
6天前
|
Java 调度
Java-Thread多线程的使用
这篇文章介绍了Java中Thread类多线程的创建、使用、生命周期、状态以及线程同步和死锁的概念和处理方法。
Java-Thread多线程的使用
|
9天前
|
Java 调度 开发者
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java多线程编程的核心概念和实际应用,通过浅显易懂的语言解释多线程的基本原理,并结合实例展示如何在Java中创建、控制和管理线程。我们将从简单的线程创建开始,逐步深入到线程同步、通信以及死锁问题的解决方案,最终通过具体的代码示例来加深理解。无论您是Java初学者还是希望提升多线程编程技能的开发者,本文都将为您提供有价值的见解和实用的技巧。
15 2
|
11天前
|
Java 数据处理
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java中的多线程编程,涵盖其基本概念、创建方法、同步机制及实际应用。通过对多线程基础知识的介绍和具体示例的演示,希望帮助读者更好地理解和应用Java多线程编程,提高程序的效率和性能。
19 1
|
4天前
|
Java 数据中心 微服务
Java高级知识:线程池隔离与信号量隔离的实战应用
在Java并发编程中,线程池隔离与信号量隔离是两种常用的资源隔离技术,它们在提高系统稳定性、防止系统过载方面发挥着重要作用。
5 0
|
6天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
7天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
9天前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
下一篇
无影云桌面