曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数


一、前言

这些天忙着写业务代码,曹工说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