多线程保证并发顺序执行的细节
背景
一个公共类存储互斥资源(3方法,分别打印1,2,3),基于同一个该类实例,创建三个并发线程t1,t2,t3,t1调用实例的printFirst()方法打印1,t2调用实例的printSecond()方法打印2,t3调用实例的printThird()方法打印3,如何保证三个并发线程顺序执行成功打印出1,2,3呢?
实现
这个问题思路很简单,在JUC当中属于入门级别了,面试中常问的“3个并发线程顺序打印1-100”也和这个差不多。
创建变量flag作为互斥变量(标记线程号),当flag=1,则表示当前方法只轮到线程1执行,flag=2,则表示当前方法只轮到线程2执行,flag=3,则表示当前方法只轮到线程3执行,以此保证打印方法的顺序性。例如当线程3提前执行了,发现当前公共类中的flag=1(即线程1还没执行打印),那么就说明来早了,得候着。
于是乎直接在公共类中创建一把锁,并使用synchronized 关键字,来做到这个效果,如下所示
public class ShareClass {
private int flag = 1; //标记下一个应该执行的线程号
private final Object obj = new Object();//创建一把锁
public void printFirst() throws InterruptedException {
synchronized(obj){
while(this.flag != 1){
obj.wait();//来早了就等着,阻塞在此
}
System.out.println("1");
this.flag = 2;//1已经打印好了,修改下一个应该执行的线程号
obj.notifyAll();//唤醒其他阻塞线程
}
}
public void printSecond() throws InterruptedException {
synchronized(obj){
while(this.flag != 2){
obj.wait();//来早了就等着,阻塞在此
}
System.out.println("2");
this.flag = 3;//2已经打印好了,修改下一个应该执行的线程号
obj.notifyAll();//唤醒其他阻塞线程
}
}
public void printThird() throws InterruptedException {
synchronized(obj){
while(this.flag != 3){
obj.wait();//来早了就等着,阻塞在此
}
System.out.println("3");
obj.notifyAll();//唤醒其他阻塞线程
}
}
}
创建main方法测试试试
public class TestDemo{
public static void main(String[] args) throws InterruptedException {
final ShareClass s= new ShareClass();
Thread t1 = new Thread(){
@Override
public void run() {
try {
s.printFirst();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
try {
s.printSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t3 = new Thread(){
@Override
public void run() {
try {
s.printThird();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t3.start();
t2.start();
t1.start();
}
}
测试结果完全没问题。
细节
为什么下面的代码中,要用 while 而不是 if ? if不行吗?
synchronized(obj){
while(this.flag != 1){
obj.wait();//来早了就等着,阻塞在此
}
this.flag = 2;//1已经打印好了,修改下一个应该执行的线程号
obj.notifyAll();//唤醒其他阻塞线程
}
这就要理解一下obj.wait();
这句代码的含义了,这句代码的意思是阻塞当前线程,释放锁,等待被唤醒再重新执行,而被唤醒的线程需要重新竞争锁对象,重新获得锁后则是从wait处继续往下执行。
那么假如把 while 换成 if 的话,我们来考虑以下情况:
假设线程3先拿到锁,此时发现flag=1,来早了,开始阻塞,释放锁。
接着线程1抢到了锁,发现此时flag=1,正常打印1,并且将flag修改为2,并唤醒其他阻塞线程,线程执行完毕,关键点来了。
此时线程3被唤醒,和线程2重新竞争锁,假设此时线程3再次抢到了锁,它会直接往下执行代码,“从wait处继续往下执行”,此时flag=2,原本应由线程2来打印,但是此时跳过判断了,直接打印出了3,接着再次释放锁,线程执行完毕。
此时当锁落到线程2手中时,3已经被打印了,于是打印出了1,3,2的情况出来了。
所以判断flag处使用 while 而不是 if 这个细节,你get到了吗?可以体会一下…
3个线程轮流打印1-10
最后贴一个3个线程轮流打印1-10的代码:
public class Print1To10 {
public static final Object lock = new Object();
public static int state = 0;
public static void print1To10(int threadId){
synchronized (lock) {
while (state < 10) {
while (state % 3 != threadId) {
try {
lock.wait();
// 避免睡醒后,发现已经不需要打印了
if(state >= 10)
return;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+": "+(state+1));
state++;
lock.notifyAll();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> print123(0));
Thread t2 = new Thread(() -> print123(1));
Thread t3 = new Thread(() -> print123(2));
t3.start();
t1.start();
t2.start();
t1.join();
t2.join();
t3.join();
System.out.println("state:"+state);
}
}