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

1、前言

這些天忙着寫業務代碼,曹工說Tomcat系列暫時沒時間寫,先隨便寫點其餘的。html

逛博客園的時候,發現一篇園友的阿里面試文章,http://www.javashuo.com/article/p-aaxpqizc-kz.htmljava

裏面提到了:兩個線程,交替打印奇偶數這道筆試題。git

看了園友實現的代碼(https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/actual/TwoThread.java),感受有點複雜,因而本身琢磨着寫了一下,如下三個版本,一個基於object的wait、notify,一個基於volatile變量的方式,最後一種和第二種類似,只是用了unsafe實現。github

 

2、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 鎖來實現阻塞和喚醒。面試

可是我我的感受,頻繁地阻塞和喚醒,都須要線程從用戶態轉入核心態,有點太耗性能了,而後寫了如下的自旋非阻塞版本。ide

 

 

3、volatile 非阻塞方式

該方式的思路是,線程在volatile變量上無限循環,直到volatile變量變爲false。變爲false後,線程開始真正地執行業務邏輯,打印數字,最後,須要掛起本身,並修改volatile變量,來喚醒其餘線程。oop

 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 }

 

3、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語言快忘完了。。)atom

 

ps:注意上面變紅部分,由於是static field,因此要用這個方法,不然用 public native long objectFieldOffset(Field var1)。

4、總結

可重入鎖的實現方式相似,這裏留給讀者進行實踐。 你們有什麼好的思路,能夠在下方進行評論,也歡迎加羣探討。

相關文章
相關標籤/搜索