曹工雜談:一道阿里面試題,兩個執行緒交替列印奇偶數
一、前言
這些天忙著寫業務程式碼,曹工說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)。
四、總結
可重入鎖的實現方式類似,這裡留給讀者進行實踐。 大家有什麼好的思路,可以在下方進行評論,也歡迎加群探討。
&n