java多线程两个线程交叉打印1到100的数字
⽬录
注意:本⽂参考
具体题⽬是这样的,两个线程交替按顺序输出1-100,第⼀个线程只能输出偶数,第⼆线程输出奇数,想象下两个⼩孩轮流喊数。
两个线程交替输出,这就意味着它俩是需要协同的,协同意味着⼆者之间要有信息传递,如何相互传递信息? 你可能直接想到,既然是0-100的数按顺序交替输出,那么每个进程只需要时不时看看计数器的值,然后看是否轮到⾃⼰输出了就⾏。没错,这就是解法⼀的思路。
解法1 静态原⼦变量死循环一个线程可以包含多个进程
有了上⾯的思路,你肯定能快速写出以下代码:
public class PrintNumber extends Thread {
private static int cnt = 0;
private int id;  // 线程编号
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (cnt < 100) {
while (cnt%2 == id) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
但当你实际运⾏后会发现
thread_0 num:1
thread_0 num:3
thread_1 num:3
thread_1 num:5
thread_1 num:6
thread_0 num:5
thread_0 num:8
thread_0 num:9
thread_1 num:8
thread_0 num:11
thread_1 num:11
.........
不仅顺序不对,还有重复和丢失!问题在哪?回到代码中cnt++; System.out.println("thread_" + id + " num:" + cnt); 这两⾏,它主要包含两个动作,cnt++和输出,当cnt++执⾏完成后可能就已经触发了另⼀个线程的输出。简化下执⾏流程,每个时刻JVM有4个动作要执⾏。
thread_0 cnt++
thread_0 print
thread_1 cnt++
thread_1 print
根据Java as-if-serial语义,jvm只保证单线程内的有序性,不保证多线程之间的有序性,所以上⾯4个步骤的执⾏次序可能是 1 2 3 4,也可能是1 3 2 4,更可能是1 3 4 2,对于上⾯的代码⽽⾔就是最终次序可能会发⽣变化。另外,cnt++ 可以拆解为两⾏底层指令,tmp = cnt + 1; cnt = tmp,当两个线程同时执⾏上述指令时就会⾯临和1 2 3 4步骤同样的问题,…… 没错,多线程下的⾏为,和你⼥朋友的⼼思⼀样难以琢磨。 如何解决这个问题?解决⽅案本质上都是保证代码执⾏顺和我们预期的⼀样就⾏,正确的解法⼀和后⾯⼏个解法本质上都是同样的原理,只是实现⽅式不⼀样。
public class PrintNumber extends Thread {
private static AtomicInteger cnt = new AtomicInteger();
private int id;
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (() <= 100) {
while (()%2 == id) {
System.out.println("thread_" + id + " num:" + ());
cnt.incrementAndGet();
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
上⾯代码通过AtomicInteger的incrementAndGet⽅法将cnt++的操作变成了⼀个原⼦操作,避免了多线
程同时操作cnt导致的数据错误,另外,while (()%2 == id也能保证只有单个线程才能进⼊while循环⾥执⾏,只有当前线程执⾏完inc后,下⼀个线程才能执⾏print,所以这个代码是可以满⾜我们交替输出的需求的。 但是,这种⽅法很难驾驭,如果说我吧run函数写成下⾯这样:
@Override
public void run() {
while (() <= 100) {
while (()%2 == id) {
cnt.incrementAndGet();
System.out.println("thread_" + id + " num:" + ());
}
}
}
只需要把print和cnt.incrementAndGet()换个位置,结果就完全不⼀样了,先inc可能导致在print执⾏前下⼀个线程就进⼊执⾏改变了cnt 的值,导致结果错误。另外这种⽅法其实也不是严格正确的,如果不是print⽽是其他类似的场景,可能会出问题,所以这种写法强烈不推荐。
解法2 静态变量死循环/wait 锁住class
事实上,我们只需要cnt++和print同时只有⼀个线程在执⾏就⾏了,所以我们可以简单将⽅法⼀中错误的⽅案加上synchronized即可,代码如下:
private static int cnt = 0;
private int id;  // 线程编号
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (cnt <= 100) {
while (cnt%2 == id) {
synchronized (PrintNumber.class) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
}
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
这⾥我⽤了synchronized关键词将cnt++和print包装成了⼀个同步代码块,可以保证只有⼀个线程可以执⾏。这⾥不知道有没有⼈会
问,cnt需不需要声明为volatile,我的回答是不需要,因为synchronized可以保证可见性。
⼤家有没有发现,我上⾯代码中⼀直都⽤了while (()%2 == id)来判断cnt是否是⾃⼰要输出的数字,这就好⽐两个⼩孩轮流报数,每个⼩孩都要耗费精⼒时不时看看是否到⾃⼰了,然后选择是否报数,这样显然太低效了。能不能两个⼩孩之间相互通知,⼀个⼩孩报完就通知下另⼀个⼩孩,然后⾃
⼰休息,这样明显对双⽅来说损耗的精⼒就少了很多。如果我们代码能有类似的机制,这⾥就能损耗更少的⽆⽤功,提⾼性能。
这就得依赖于java的wait和notify机制,当⼀个线程执⾏完⾃⼰的⼯作,然后唤醒另⼀个线程,⾃⼰去休眠,这样每个线程就不⽤忙等。代码改造如下,这⾥我直接去掉了while (()%2 == id)。
@Override
public void run() {
while (cnt <= 100) {
synchronized (PrintNumber.class) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
ify();
try {
PrintNumber.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
解法3 静态变量 ReentrantLock Condition
能⽤synchronized的地⽅就能⽤ReentrantLock,所以解法三和解法⼆本质上是⼀样的,就是把synchronized换成了lock⽽已,然后把wait和notify换成Condition的signal和await,改造后的代码如下:
private static Lock lock = new ReentrantLock();
private static Condition condition = wCondition();
private int id;
private static int cnt = 0;
public PrintNumber(int id) {
this.id = id;
}
private static void print(int id) {
}
@Override
public void run() {
while (cnt <= 100) {
lock.lock();
System.out.println("thread_" + id + " num:" + cnt);
cnt++;
condition.signal();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
到这⾥我所能想到的解法就这么多了,不知道你们还有没有其他的解法,接下来我出⼏道扩展问题,希望能帮你更深⼊理解这个题⽬涉及到的多线程的知识点。
扩展问题
1. 如果是三个线程交替输出呢?
解析:三个线程的解法可以使⽤while (cnt%3 == id)的⽅式实现忙等,但简单的唤醒+等待的⽅式必然不适⽤了, 没有判断的synchronized 必然实现不了,java Object的notify和wait⽅法只能唤醒全部线程,然后另外两个线程输出前都需要额外判断下是否轮到⾃⼰输出了。这时候lock中condition的优势就体现出来了,它可以通过设置不同的condition来实现不同线程的精确唤醒。
2. ⽣产者消费者
解析:两个线程按顺序交替输出本质上就是多线程之间的相互协同,⽽这个领域另外⼀个⾮常有名且更常见的问题就是⽣产者消费者问题,两个线程按顺序交替输出你可以认为是当⽣产者和单消费者的⼀种特殊情况