目录
一、基础
二、进阶
一、基础
有三个线程,线程名称分别为: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()` 方法等待另外两个线程执行完毕后再继续执行,会导致线程的阻塞和唤醒操作频繁地发生,从而影响程序的性能。
况且,实现起来也并不那么直接。以下的几种做法都是不正确的:
相比之下,在这个场景中,使用 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会同时执行打印操作。这样就会打乱打印顺序。
为了避免这种情况,我们使用 while 循环来判断条件是否满足。在使用 while 循环时,线程会在被唤醒后再次检查条件是否满足,如果不满足则继续等待,从而避免了虚假唤醒的问题。