曹工雜談:一道阿里面試題,兩個線程交替打印奇偶數


一、前言

這些天忙着寫業務代碼,曹工說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實現。

 

update on 2020/6/7,下面的第二種方式,現在回頭看,其實感覺寫得不好,下面直接貼一種更直接的方式(目前的技術水平寫的,應該比之前的寫的好點)。

 1 @Slf4j
 2 public class OddEvenDemo {
 3     private static volatile int number = 0;
 4 
 5     public static void main(String[] args) {
 6         final Object monitor = new Object();
 7 
 8         /**
 9          * 奇數線程
10          */
11         Runnable callable = new Runnable() {
12 
13             @Override
14             public void run() {
15                 while (true) {
16                     boolean interrupted = Thread.currentThread().isInterrupted();
17                     if (interrupted) {
18                         break;
19                     }
20                     synchronized (monitor) {
21                         while (number % 2 == 0) {
22                             try {
23                                 monitor.wait();
24                             } catch (InterruptedException e) {
25                                 e.printStackTrace();
26                             }
27                         }
28                         log.info("奇數線程, number:{}", number);
29                         number++;
30 
31                         monitor.notify();
32                     }
33 
34                     try {
35                         Thread.sleep(5000);
36                     } catch (InterruptedException e) {
37                         e.printStackTrace();
38                     }
39                 }
40             }
41         };
42         Thread thread1 = new Thread(callable);
43         thread1.setName("odd");
44         thread1.start();
45 
46         /**
47          * 偶數線程
48          */
49         Runnable evenCallable = new Runnable() {
50 
51             @Override
52             public void run() {
53                 while (true) {
54                     boolean interrupted = Thread.currentThread().isInterrupted();
55                     if (interrupted) {
56                         break;
57                     }
58                     synchronized (monitor) {
59                         while (number % 2 != 0) {
60                             try {
61                                 monitor.wait();
62                             } catch (InterruptedException e) {
63                                 e.printStackTrace();
64                             }
65                         }
66                         log.info("偶數線程, number:{}", number);
67                         number++;
68 
69                         monitor.notify();
70                     }
71 
72 
73                     try {
74                         Thread.sleep(5000);
75                     } catch (InterruptedException e) {
76                         e.printStackTrace();
77                     }
78                 }
79             }
80         };
81         Thread thread = new Thread(evenCallable);
82         thread.setName("even");
83         thread.start();
84 
85 
86     }
87 }

 

二、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)。

四、總結

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM