一、前言
這些天忙着寫業務代碼,曹工說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)。
四、總結
可重入鎖的實現方式類似,這里留給讀者進行實踐。 大家有什么好的思路,可以在下方進行評論,也歡迎加群探討。