三个线程交叉打印ABC(synchronized、wait/notify)
这是以前在面试美团Java实习生岗位时被问到的一道题,当时没有写出来,后面在网上看别人代码也是云里雾里,感觉每个人写的也都不一样,没有强调重点,导致看的越多就感觉越乱,有人用一把锁,也有人用三把锁。这次我尽力把交叉打印讲清楚,把重点都标注出来。
首先,线程打印字母是线程在执行一个任务,所以我们需要定义三个任务(Runnable),然后交给三个线程去执行(Thread)。
我们要明白三个线程交叉打印,这三个线程之间时需要相互通信的,也就是说线程1打印完A之后,需要通知线程2去打印B,线程2打印完B之后,需要通知线程3去打印C,以此类推。好了,我们现在知道线程之间是要相互通信的,那么线程之间要怎么通信呢?可共享变量是Java给我提供的天然的通信媒介,线程可以通过读取共享变量的内容,然后线程本身来决定在线程中进行什么操作。
这个共享变量就是作为锁对象,只有获取到了锁对象才可以执行任务中的逻辑,否则任务会被阻塞。
由于代码在执行的过程中会出现原子性、可见性、指令重排等情况,所以Java提供了如synchronized关键字来保证线程通信时的正确性。如果不清楚原子性、可见性、指令重排也没有关系,只要清楚synchronized可以保证线程之间在通信时的正确性就行了,synchronized的作用就是加锁。
wait/notify都是对于锁对象的操作,不是执行任务的线程来进行wait/notify,这一点非常重要!!!锁对象的作用是持有锁对象的线程才可以进行打印,如果持有锁对象的线程不满足打印条件(条件在代码中有说明),那么就需要让出锁对象,让出锁对象的操作就是wait,然后通知其他线程来竞争锁对象,通知的操作就是notify,记住wait/notify都是由锁对象来执行的!!!
好了,我们现在明白我们需要干什么了:定义三个任务,然后交给三个线程去执行、使用一个共享变量来在线程中进行通信、用synchronized来保证线程通信时的正确性。
接下来,我们来看代码:
public class Test07 {
//定义一个共享变量,用来在线程中进行通信
private static final Object obj = new Object();
//定义一个变量来记录打印的次数,控制打印条件
private static int count = 1;
public static void main(String[] args) {
//创建三个线程,然后把三个任务分别放入这三个线程执行
//创建线程1执行任务1
new Thread(new Task1()).start();
//创建线程2执行任务2
new Thread(new Task2()).start();
//创建线程3执行任务4
new Thread(new Task3()).start();
}
//任务1
private static class Task1 implements Runnable {
@Override
public void run() {
synchronized (obj) {
//打印十次A
for (int i = 0; i < 10; i++) {
//一直轮询,如果条件不是要打印A的条件,那么直接释放锁
/*
这个地方一定要用while,如果用了if的话,再下次重新获得锁的时候,是继续往下去执行,走到obj.notifyAll()这一行代码,不会再判断count的值,
用while的话会再去判断count的值,如果符合条件再往下执行,走到obj.notifyAll()这一行代码,可以自己把while换成if试试,看看结果
*/
while (count % 3 != 1) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知其他所有线程可以来抢占锁了,但是发现现在线程1在持有锁,其他线程还抢不到,只有等到线程1释放锁之后,才可以抢到锁
obj.notifyAll();
System.out.println("A");
count++;
}
}
}
}
//任务2
private static class Task2 implements Runnable {
@Override
public void run() {
synchronized (obj) {
//打印十次B
for (int i = 0; i < 10; i++) {
//一直轮询,如果条件不是要打印B的条件,那么直接释放锁
while (count % 3 != 2) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知其他所有线程可以来抢占锁了,但是发现现在线程2在持有锁,其他线程还抢不到,只有等到线程2释放锁之后,才可以抢到锁
obj.notifyAll();
System.out.println("B");
count++;
}
}
}
}
//任务3
private static class Task3 implements Runnable {
@Override
public void run() {
synchronized (obj) {
//打印十次C
for (int i = 0; i < 10; i++) {
//一直轮询,如果条件不是要打印C的条件,那么直接释放锁
while (count % 3 != 0) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知其他所有线程可以来抢占锁了,但是发现现在线程3在持有锁,其他线程还抢不到,只有等到线程3释放锁之后,才可以抢到锁
obj.notifyAll();
System.out.println("C");
count++;
}
}
}
}
}