一、前言
这些天忙着写业务代码,曹工说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)。
四、总结
可重入锁的实现方式类似,这里留给读者进行实践。 大家有什么好的思路,可以在下方进行评论,也欢迎加群探讨。