我已经在语雀上介绍了相关两阶段终止模式,具体文章见:终止模式之两阶段终止模式
一、volatile改进两阶段终止模式
1.1 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test09") public class Test09 { public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start();
Thread.sleep(3500); log.debug("停止监控"); tpt.stop(); } }
@Slf4j(topic = "c.TwoPhaseTermination") class TwoPhaseTermination { private Thread monitorThread; private volatile boolean stop = false;
public void start() { monitorThread = new Thread(() -> { while (true) { if(stop){ log.debug("料理后事"); break; }
try { Thread.sleep(1000); log.debug("执行监控记录"); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1"); monitorThread.start(); }
public void stop(){ stop = true; monitorThread.interrupt(); } }
|
运行结果
1 2 3 4 5 6 7 8 9
| 09:55:59.124 c.TwoPhaseTermination [t1] - 执行监控记录 09:56:00.131 c.TwoPhaseTermination [t1] - 执行监控记录 09:56:01.132 c.TwoPhaseTermination [t1] - 执行监控记录 09:56:01.623 c.Test09 [main] - 停止监控 java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.lilinchao.concurrent.demo_03.TwoPhaseTermination.lambda$start$0(Test09.java:40) at java.lang.Thread.run(Thread.java:748) 09:56:01.624 c.TwoPhaseTermination [t1] - 料理后事
|
代码分析
TwoPhaseTermination
类中调用start()方法,启动一个t1
线程。
- t1线程中while(true)方法循环执行监控记录,使得t1线程无法结束。
- stop方法初始值为false,t1线程刚启动时,不会进入
if(stop)
条件判断中执行break;
退出循环。
- stop()方法中将成员变量stop改为true,同时调用
monitorThread.interrupt();
方法,为了打断正在睡眠中的t1线程,使其停止睡眠立即向下执行。
- 要想使t1线程能够读取到成员变量stop在主线程中的更改,加上关键字
volatile
执行过程分析
- 主线程中调用
TwoPhaseTermination
类的start()方法,开启t1线程;
- t1线程中每睡眠1s控制台输出一次“执行监控记录”;
- 主线程在3.5秒后调用stop()方法,将stop设置成true,准备停止t1线程;
- 此时t1线程已经执行完第三轮循环,正处于第四轮循环的睡眠状态,但是被
monitorThread.interrupt();
方法打断睡眠,抛出一个打断异常后,开始第五轮循环;
- 此时stop成员变量为true,进入if判断条件,料理后事后退出循环;
- t1线程运行结束,退出。
1.2 该方式存在的缺陷
当我们多次调用start()方法时,会创建多个t1线程,整个线程都在重复执行,消耗资源且没有意义。
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); tpt.start();
Thread.sleep(3500); log.debug("停止监控"); tpt.stop(); }
|
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 10:04:33.048 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:33.049 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:34.052 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:34.052 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:35.052 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:35.052 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:35.547 c.Test09 [main] - 停止监控 java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.lilinchao.concurrent.demo_03.TwoPhaseTermination.lambda$start$0(Test09.java:41) at java.lang.Thread.run(Thread.java:748) 10:04:35.549 c.TwoPhaseTermination [t1] - 料理后事 10:04:36.053 c.TwoPhaseTermination [t1] - 执行监控记录 10:04:36.053 c.TwoPhaseTermination [t1] - 料理后事
|
问题?
如何保证某个方法被多次调用时,只会执行一次,下次在执行直接返回,不再继续向下执行。
可以通过Balking模式来实现。
二、Balking模式
2.1 定义
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
2.2 实现
Balking模式就是加入一个全局变量starting,通过starting的状态来判断该线程是否被调用过
1
| private volatile boolean starting;
|
在调用方法中加入条件判断
1 2 3 4
| if (starting) { return; } starting = true;
|
- 当方法第一次被调用,starting变量为false,不进入条件判断语句,继续向下执行,这时将starting状态改为true;
- 当方法已经被调用过时,此时状态值starting被改为true,进入到条件判断当中,执行return,退出该方法。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class MonitorService { private volatile boolean starting; public void start() { log.info("尝试启动监控线程..."); synchronized (this) { if (starting) { return; } starting = true; }
} }
|
思考:为什么需要在Balking模式代码中加上synchronized锁?
1 2 3 4
| if (starting) { return; } starting = true;
|
因为上方几段代码即涉及到了读,也涉及到了写,对于多行语句,在多线程情况下,保证的就不仅仅是可见性,必须得保证代码的原子性。所以,上方几段代码必须放在synchronized代码块中。
三、Balking模式应用
对上方volatile实现两阶段终止模式代码通过Balking模式进行改进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test09") public class Test09 { public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); tpt.start(); tpt.start();
} }
@Slf4j(topic = "c.TwoPhaseTermination") class TwoPhaseTermination { private Thread monitorThread; private volatile boolean stop = false; private boolean starting = false;
public void start() { synchronized (this){ if (starting) { return; } starting = true; } monitorThread = new Thread(() -> { while (true) { if(stop){ log.debug("料理后事"); break; }
try { Thread.sleep(1000); log.debug("执行监控记录"); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1"); monitorThread.start(); }
public void stop(){ stop = true; monitorThread.interrupt(); } }
|
运行结果
1 2 3 4 5 6 7 8 9
| 10:44:22.543 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:23.546 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:24.547 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:25.547 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:26.561 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:27.561 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:28.562 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:29.562 c.TwoPhaseTermination [t1] - 执行监控记录 10:44:30.563 c.TwoPhaseTermination [t1] - 执行监控记录
|
从结果可以看出,每隔1s输出一次记录,不会再像之前一样,每秒打印多次记录。