🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈
文章目录
- 1. 如何实现三个线程顺序打印ABC
- 1.1 详细要求描述
- 1.2 使用join()
- 1.3 实现代码
- 1.4 运行结果
- 2. 如何实现三个线程循环打印ABC
- 2.1 详细要求描述
- 2.2 使用wait()和notifyAll()方法
- 2.3 补充:竞态条件
- 2.4 实现代码
- 2.5 运行结果
- 2.6 为什么使用这些 —— 细节探讨
- 2.6.1 为什么用 notifyAll,而不是 notify
- 2.6.2 为什么用 while(count % 3 != ?) 判断,而不是 if
1. 如何实现三个线程顺序打印ABC
1.1 详细要求描述
描述:实现三个线程按顺序打印 ABC,并且只打印一次!
输出:ABC
1.2 使用join()
这里很容易想到使用 join() 方法
在 Thread类及其基本用法 这期内容中详细介绍 join()方法
【join()方法的作用】
线程的调度是无序的,哪个线程先执行,我们无从知晓,而使用 join() 方法,可以明确控制线程结束的执行顺序!
需要理解其用法:在哪个线程调用该线程的join()方法,就是让哪个线程等待该线程结束
通过 join() 方法,可以实现线程之间的同步,如在 t1 线程中,调用 t2.join() 即让 t1 线程 等待 t2 线程结束,t1 线程阻塞等待,而其它线程正常调度,这样 t2 线程先执行,t1 线程会等待 t2 线程执行完毕后再继续执行,明确控制了线程结束执行顺序
1.3 实现代码
public class PrintABC1 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{System.out.println("A");});Thread t2 = new Thread(()->{System.out.println("B");});Thread t3 = new Thread(()->{System.out.println("C");});t1.start();t1.join();t2.start();t2.join();t3.start();t3.join();}
}
1.4 运行结果
符合预期:
2. 如何实现三个线程循环打印ABC
2.1 详细要求描述
描述:有三个线程,每个线程只能打印一个字母,分别为 A,B,C,要求按照这个 ABC 的顺序,打印 10 次!
输出:A B C A B C A B C A B C A B C A B C A B C A B C A B C A B C
2.2 使用wait()和notifyAll()方法
我们可能会想:
- 使用 join() ? (×)
在这个场景中,特点是循环打印,而不是打印一次,在循环打印的条件下,其实join()方法就不太适用了,因为线程 t1、t2、t3 里的代码需要循环执行多次,join()方法会阻塞当前线程直到另一个线程完成,如果按顺序在每个线程打印后调用 join(),虽然可以确保执行的顺序,但是不灵活且效率低下,尤其是需要多次重复执行的时候,且不好实现 - 使用 sleep() ?(×)
仍然无法保证执行顺序,具有不确定性,sleep()方法只是让当前线程暂停执行指定的时间,不能确保其它线程在这段时间内完成了工作!!!,这可能导致 “竞态条件” ,同样,效率比较低下,因为它需要猜测其他线程完成所需的时间,通常是不准确(这怎么好猜!)
相比之下,在上述场景中,使用 wait()方法和notifyAll()方法可以更好实现线程之间的同步!可以让线程在需要等待的时候进入等待状态,当满足某个条件的时候,再唤醒该线程,执行其内容
在 wait()和notify() 这期内容中,具体介绍了 wait()和notifyAll()方法
循环打印ABC,具体思路如下:
1) 定义共享变量:
- count:用于控制打印的次数,初始化为1
- MAX_COUNT:定义打印的总次数,为10
2)创建并启动线程:
- 创建三个线程t1,t2,t3,分别代表打印A,B, C的线程
- 每个线程内部都通过一个for循环来控制打印的次数,循环条件是 i < abc.MAX_COUNT,实际的打印次数控制是通过 count 变量和wait()、notifyAll()来实现的
3)线程同步和通信以及打印操作:
- 创建一个锁对象,abc,其中 abc 是PrintABC类的一个实例,使用synchronized (abc)来确保在同一时刻只有一个线程可以执行打印操作
- 在每个线程中,通过 while 循环检查count % 3 的值来确定当前线程是否应该执行打印操作
- 如果当前线程不应该执行打印,则调用abc.wait()进入等待状态,直到其他线程调用abc.notifyAll()唤醒它
- 当线程执行完打印操作后,将count+1,并调用abc.notifyAll()唤醒所有可能因条件不满足而等待的线程
4)输出结果:
最终,程序将输出10次A B C的序列,每次中A,B,C 按顺序被打印出来
【图解说明】
2.3 补充:竞态条件
【含义】指在多线程环境中,两个或多个线程在访问共享资源时,由于执行顺序的不确定性而导致的程序输出结果与预期不符或程序状态错误的现象,即由于线程执行的时间顺序不一致,导致程序的行为变得不可预测
【可能发生的情况】
- 多个线程同时读写共享资源:当多个线程对同一个资源进行读和写操作时,如果没有适当的同步机制来控制访问顺序,就可能导致数据不一致
- 条件竞争:线程之间在执行顺序上存在竞争关系,某个线程的执行结果依赖于其他线程的执行进度,如果这种依赖关系没有得到妥善处理,就很容易导致竞态条件
…
【竞态条件可能产生的影响】
- 数据不一致:共享资源中的数据被错误地修改,导致数据与预期的不符合
- 程序崩溃:在某些情况下,竞态条件可能导致程序崩溃或异常终止
- 死锁:在复杂的同步机制中,竞态条件可能引发死锁
…
【如何避免竞态条件】
通常需要使用同步机制来控制线程对共享资源的访问,这些同步机制包括有:
- 互斥锁(Mutexes)
- 信号量(Semaphores)
- 读写锁(Read-Write Locks)
- 条件变量(Condition Variables)
- Java 中的 synchronized关键字、ReentrantLock类
2.4 实现代码
public class PrintABC {private int count = 1; // 控制打印的轮次private final int MAX_COUNT = 10; // 打印的总轮次public static void main(String[] args) {PrintABC abc = new PrintABC();Thread t1 = new Thread(() -> {for (int i = 0; i < abc.MAX_COUNT; ) {synchronized (abc) {while (abc.count % 3 != 1) {try {abc.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("A ");abc.count++;abc.notifyAll();i++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < abc.MAX_COUNT; ) {synchronized (abc) {while (abc.count % 3 != 2) {try {abc.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("B ");abc.count++;abc.notifyAll();i++;}}});Thread t3 = new Thread(() -> {for (int i = 0; i < abc.MAX_COUNT; ) {synchronized (abc) {while (abc.count % 3 != 0) {try {abc.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("C ");abc.count++;abc.notifyAll();i++;}}});t1.start();t2.start();t3.start();}
}
2.5 运行结果
符合预期:
2.6 为什么使用这些 —— 细节探讨
2.6.1 为什么用 notifyAll,而不是 notify
在上述代码中,使用的是notifyAll()方法来通知其它所有线程,而并不是使用notify()方法,因为在上述代码中,并不是只是涉及到两个线程,涉及到三个线程,当 t1 线程执行时候,t2、t3 线程条件不满足,都处于 wait 状态,notify()方法只会随机唤醒一个等待该锁对象的线程,notifAll()方法则是唤醒所有等待该对象锁的线程,因为我们期望通知到所有的等待线程,再根据条件判断,是否继续等待,还是可以被唤醒执行,因此,在这里使用 notifyAll()方法!
2.6.2 为什么用 while(count % 3 != ?) 判断,而不是 if
其实在之前的内容中,提到过~ 不知道各位小伙伴还有木有印象,可以回顾这期内容:阻塞队列,改进优化,将 if 改为 while!
为什么要这样做呢?
因为 Java官方并不建议这么使用 wait()方法,即并不建议使用 if 判断,而是使用 while 判断,尽管这里的三个线程并没有暗含着interrupt() 方法,但使用while可以避免虚假唤醒!
可以看到如果把 while 改成 if,顺序是有问题的:
我们可以进行仔细分析,如果此时 count 为 1,则只有 A 可以打印,B 和 C 因为唤醒条件不满足,进入 wait 等待状态,如果 B 和 C 可以打印,则是虚假唤醒,试想一下,如果是 if 判断,进行了一次 if 判断之后,B 和 C 进入 wait 等待状态,等到 A 执行完毕后,此时 count+1, count 为 2,唤醒所有线程,此时打印 B,C 应该进入等待状态,但是之前已经判断过一次,所以A 和 C 也被唤醒了,此时就是虚假唤醒,和 B 所在的线程抢占式执行,此后的 t1、t2、t3 线程执行顺序也就不可控了
因此,这里需要,在 wait()方法唤醒后,再判定一次条件,wait() 之前,发现条件不满足,开始 wait(),等到 wait() 唤醒后再确认一下条件是否满足,如果不满足,还可以继续 wait()!故用 while 循环判断~
✨✨✨本期内容到此结束啦~ 下期再见!