曹工雜談:一道阿里面試題,兩個執行緒交替列印奇偶數

三國夢迴發表於2019-07-20

一、前言

這些天忙著寫業務程式碼,曹工說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 }

 

 

 

思路很簡單,程式碼也很簡單,主要就是基於 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)。

四、總結

可重入鎖的實現方式類似,這裡留給讀者進行實踐。 大家有什麼好的思路,可以在下方進行評論,也歡迎加群探討。

 

相關文章