曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数

一、前言

这些天忙着写业务代码,曹工说Tomcat系列暂时没时间写,先随便写点其他的。

逛博客园的时候,发现一篇园友的阿里面试文章,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 }

 

曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数_第1张图片

 

 

思路很简单,代码也很简单,主要就是基于 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语言快忘完了。。)

 

ps:注意上面变红部分,因为是static field,所以要用这个方法,否则用 public native long objectFieldOffset(Field var1)。

四、总结

可重入锁的实现方式类似,这里留给读者进行实践。 大家有什么好的思路,可以在下方进行评论,也欢迎加群探讨。

 

你可能感兴趣的:(曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数)