曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数
一、前言
逛博客园的时候,发现一篇园友的阿里面试文章,https://www.cnblogs.com/crossoverJie/p/9404789.html。
里面提到了:两个线程,交替打印奇偶数这道笔试题。
看了园友实现的代码(https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/actual/TwoThread.java),感觉有点复杂,于是自己琢磨着写了一下,以下三个版本,一个基于object的wait、notify,一个基于volatile变量的方式,最后一种和第二种相似,只是用了unsafe实现。
二、object的wait/notify方式
1 package producerconsumer;
2
3 import java.util.concurrent.atomic.AtomicInteger;
4
5 public class OddEvenThread {
6 private static volatile Integer counter = 0;
7 private static Object monitor = new Object();
8
9 public static void main(String[] args) {
10 new Thread(new Runnable() {
11 // 奇数线程
12 @Override
13 public void run() {
14 while (true){
15 synchronized (monitor){
16 if (counter % 2 != 0){
17 continue;
18 }
19 int i = ++counter;
20 if (i > 100){
21 return;
22 }
23 System.out.println("奇数线程:" + i);
24 try {
25 monitor.notify();
26 monitor.wait();
27 } catch (InterruptedException e) {
28 e.printStackTrace();
29 }
30 }
31 }
32 }
33 }).start();
34
35 new Thread(new Runnable() {
36 @Override
37 public void run() {
38 while (true){
39 synchronized (monitor){
40 if (counter % 2 == 0){
41 continue;
42 }
43 int i = ++counter;
44 if (i > 100){
45 return;
46 }
47 System.out.println("偶数线程:" + i);
48 try {
49 monitor.notify();
50 monitor.wait();
51 } catch (InterruptedException e) {
52 e.printStackTrace();
53 }
54 }
55 }
56 }
57 }).start();
58
59
60 }
61 }
思路很简单,代码也很简单,主要就是基于 synchronized 锁来实现阻塞和唤醒。
但是我个人感觉,频繁地阻塞和唤醒,都需要线程从用户态转入核心态,有点太耗性能了,然后写了以下的自旋非阻塞版本。
三、volatile 非阻塞方式
该方式的思路是,线程在volatile变量上无限循环,直到volatile变量变为false。变为false后,线程开始真正地执行业务逻辑,打印数字,最后,需要挂起自己,并修改volatile变量,来唤醒其他线程。
1 package producerconsumer;
2
3 /**
4 * Created by Administrator on 2019/7/20.
5 */
6 public class OddEvenThreadVolatileVersion {
7 private static volatile boolean loopForOdd = true;
8
9 private static volatile boolean loopForEven = true;
10
11 private static volatile int counter = 1;
12
13 public static void main(String[] args) throws InterruptedException {
14 new Thread(new Runnable() {
15
16 // 奇数线程
17 @Override
18 public void run() {
19 while (true) {
20 while (loopForOdd){
21
22 }
23
24 int counter = OddEvenThreadVolatileVersion.counter;
25 if (counter > 100) {
26 break;
27 }
28 System.out.println("奇数线程:" + counter);
29
30 OddEvenThreadVolatileVersion.counter++;
31
32 // 修改volatile,通知偶数线程停止循环,同时,准备让自己陷入循环
33 loopForEven = false;
34
35 loopForOdd = true;
36
37 }
38
39 }
40 }).start();
41
42 new Thread(new Runnable() {
43 @Override
44 public void run() {
45 while (true) {
46 while (loopForEven) {
47
48 }
49
50 int counter = OddEvenThreadVolatileVersion.counter;
51 if (counter > 100) {
52 break;
53 }
54 System.out.println("偶数线程:" + counter);
55
56 OddEvenThreadVolatileVersion.counter++;
57
58 // 修改volatile,通知奇数线程停止循环,同时,准备让自己陷入循环
59 loopForOdd = false;
60
61 loopForEven = true;
62 }
63 }
64 }).start();
65
66 // 先启动奇数线程
67 loopForOdd = false;
68
69 }
70 }
三、unsafe实现的版本
1 package producerconsumer;
2
3 import sun.misc.Unsafe;
4
5 import java.lang.reflect.Field;
6
7 /**
8 * Created by Administrator on 2019/7/20.
9 */
10 public class OddEvenThreadCASVersion {
11 private static volatile boolean loopForOdd = true;
12
13 private static volatile boolean loopForEven = true;
14
15 private static long loopForOddOffset;
16
17 private static long loopForEvenOffset;
18
19 private static volatile int counter = 1;
20
21 private static Unsafe unsafe;
22
23 static {
24 Field theUnsafeInstance = null;
25 try {
26 theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
27 } catch (NoSuchFieldException e) {
28 e.printStackTrace();
29 }
30 theUnsafeInstance.setAccessible(true);
31 try {
32 unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);
33 } catch (IllegalAccessException e) {
34 e.printStackTrace();
35 }
36
37 try {
38 loopForOddOffset = unsafe.staticFieldOffset
39 (OddEvenThreadCASVersion.class.getDeclaredField("loopForOdd"));
40 } catch (Exception ex) { throw new Error(ex); }
41
42 try {
43 loopForEvenOffset = unsafe.staticFieldOffset
44 (OddEvenThreadCASVersion.class.getDeclaredField("loopForEven"));
45 } catch (Exception ex) { throw new Error(ex); }
46 }
47
48 public static void main(String[] args) throws InterruptedException {
49 new Thread(new Runnable() {
50
51 // 奇数线程
52 @Override
53 public void run() {
54 while (true) {
55 while (true){
56 boolean b = unsafe.getBoolean(OddEvenThreadCASVersion.class, loopForOddOffset);
57 if (b){
58 // 循环
59 }else {
60 break;
61 }
62 }
63
64 int counter = OddEvenThreadCASVersion.counter;
65 if (counter > 100) {
66 break;
67 }
68 System.out.println("奇数线程:" + counter);
69
70 OddEvenThreadCASVersion.counter++;
71
72 // 修改volatile,通知偶数线程停止循环,同时,准备让自己陷入循环
73 unsafe.putBoolean(OddEvenThreadCASVersion.class, loopForOddOffset,true);
74 unsafe.putBoolean(OddEvenThreadCASVersion.class, loopForEvenOffset,false);
75
76 }
77
78 }
79 }).start();
80
81 new Thread(new Runnable() {
82 @Override
83 public void run() {
84 while (true) {
85 while (true){
86 boolean b = unsafe.getBoolean(OddEvenThreadCASVersion.class, loopForEvenOffset);
87 if (b){
88 // 循环
89 }else {
90 break;
91 }
92 }
93
94 int counter = OddEvenThreadCASVersion.counter;
95 if (counter > 100) {
96 break;
97 }
98 System.out.println("偶数线程:" + counter);
99
100 OddEvenThreadCASVersion.counter++;
101
102 // 修改volatile,通知奇数线程停止循环,同时,准备让自己陷入循环
103 unsafe.putBoolean(OddEvenThreadCASVersion.class, loopForOddOffset,false);
104 unsafe.putBoolean(OddEvenThreadCASVersion.class, loopForEvenOffset,true);
105 }
106 }
107 }).start();
108
109 // 先启动奇数线程
110 loopForOdd = false;
111
112 }
113 }
代码整体和第二种类似,只是为了学习下 unsafe 的使用。unsafe的操作方式,如果学过c语言的话,应该会觉得比较熟悉,里面的offset,其实就类似与指针的位置。
我们看看,要获取一个值,用unsafe的写法是,unsafe.getBoolean(OddEvenThreadCASVersion.class, loopForEvenOffset),模拟成c语言就是,获取到 OddEvenThreadCASVersion 的指针,再偏移 loopForEvenOffset,再取接下来的4个字节,换算成 boolean即可。
void * ptr = &OddEvenThreadCASVersion.class
int tmp = *(int*)(ptr + loopForEvenOffset)
boolean ret = (boolean)tmp;
(只是个示意,不用纠结哈,c语言快忘完了。。)
四、总结
可重入锁的实现方式类似,这里留给读者进行实践。 大家有什么好的思路,可以在下方进行评论,也欢迎加群探讨。