文章目录
1. 什么是中断机制?
首先,一个线程不应该由其他线程来强制中断或停止,应该是由线程自己自行停止的,自己来决定自己命运才合理
其次,Java没办法立刻停止一条线程,所以停止线程显得尤为重要,如取消一个耗时操作
因此,Java提供了一种用于停止线程的协商机制——中断,也称中断标识协商机制
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现
若要中断一个线程,需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设置为true
接着需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要自己写代码实现
每个线程对象都有一个中断标识位,表示该线程是否被中断;
- 如果该线程位为true表示中断
- 如果该线程位为false表示未中断
通过调用线程对象的interrupt方法可以将该线程的标识为设为true,可以在别的线程调用,也可以在自己的线程调用
2. 中断的相关API
中断相关的API有三个
- publib void interrupt()
- publib static bollean interrupted()
- publib bollean isInterrupted()
这三个方法执行的效果如下
- interrupt:实例interrupt方法仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
- interrupted:这个方法是判断线程是否中断并清除当前中断状态
- 返回当前线程的中断状态,测试当前线程是否已被中断
- 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
- 如果连续调用两次该方法,则两次调用将返回false,因为连续两次调用的结果可能不一样
- isInterrupted:判断当前线程是否被中断(通过检查中断标志位)
3. 如何停止中断运行中的线程
前面说到一个线程不应该由其他线程来强制中断或停止,应该是由线程自己自行停止的,自己来决定自己命运才合理
因此这里有三种方式实现
- 通过一个volatile变量实现
- 通过AtomicBoolean实现
- 通过Thread类自带的中断api实例方法实现
3.1 通过一个volatile变量实现
volatile的作用主要有如下两个:
- 线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
- 顺序一致性:禁止指令重排序
测试类代码
public class InterruptDemo {
static volatile Boolean isStop = false;
public static void main(String[] args) {
new Thread(()->{
while (true){
if (isStop){
System.out.println(Thread.currentThread().getName() + "\t isStop被修改为true,程序停止");
break;
}
System.out.println("t1 - hello - volatile");
}
}, "t1").start();
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(()->{
isStop = true;
}, "t2").start();
}
}
当t2线程执行将isStop设置为true的时候,t1线程就会退出循环,结束线程
3.2 通过AtomicBoolean实现
AtomicBoolean提供了一种原子性地读和写布尔类型变量的解决方案,通常情况下,该类将被用于原子性地更新状态标识位
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (atomicBoolean.get()) {
System.out.println(Thread.currentThread().getName() + "\t AtomicBoolean被修改为true,程序停止");
break;
}
System.out.println("t1 - hello - AtomicBoolean");
}
}, "t1").start();
try {
TimeUnit.MICROSECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
atomicBoolean.set(true);
}, "t2").start();
}
}
3.3 通过Thread类自带的中断api实例方法实现
通过上面三种API实现
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "\t isInterrupted被修改为true,程序停止");
break;
}
System.out.println("t1 - hello - api");
}
}, "t1");
t1.start();
System.out.println("t1 - 结束 - " + Thread.currentThread().isInterrupted());
try {
TimeUnit.MICROSECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
t1.interrupt();
}, "t2").start();
}
3.4 总结
这三种方法虽然实现方式不一样,但是本质来说都是一个线程修改另一个线程的中断标志位,接着另一个线程识别到标志位修改后,自行中断线程。
4. 当前线程的中断标识为true,是不是线程就立刻停止
如果线程处于正常活动状态下,调用interrupt方法,会将该线程的中断标志设置为true,仅此而已
被设置中断的线程仍然会继续正常运行,不会受到影响
所以interrupt方法并不能真正中断线程,需要被调用的线程自己进行配合才行
public class InterruptDemo2 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 300; i++) {
System.out.println("---------:" + i);
}
}, "t1");
t1.start();
System.out.println("t1 默认线程的中断标识02:" + t1.isInterrupted());
try {
TimeUnit.MICROSECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
System.out.println("t1 默认线程的中断标识01:" + t1.isInterrupted());
try {
TimeUnit.MICROSECONDS.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t1.interrupt();
System.out.println("t1 默认线程的中断标识03:" + t1.isInterrupted());
}
}
在这个测试类中,即使线程t1的中断标志被设置为true,不会立刻停止线程t1,t1仍然继续运行打印输出完
t1 默认线程的中断标识02:false
t1 默认线程的中断标识01:true
t1 默认线程的中断标识02:false
在设置中断标识后,休眠5s,然后再次设置中断标识位,最后输出发现中断标识位为false
这是因为5s后该线程已经运行完不活动了,而interrupt的api规定中断不活动的线程不会产生任何影响
如果线程处于阻塞状态(例如处于sleep,wait,join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将会立刻退出被阻塞状态,并抛出一个InterruptedException异常
public class InterruptDemo03 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread() + "中断标志位" + Thread.currentThread().isInterrupted() + "程序停止");
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("----hello InterruptDemo03");
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
t1.interrupt();
}, "t2").start();
}
}
当线程调用wait/sleep/wait方法的时候,那么该线程的中断状态就会被清除,并且收到InterruptedException异常
这里的案例,一开始中断标识位默认为false
t2线程向t1线程发出了中断协商,t2调用了t1的interrupt,设置t1的中断标志位为true
正常情况下,程序应该停止
异常情况下,出现InterruptedException异常,将会把中断状态清除,收到InterruptedException,中断标志位为false,导致无限循环
解决方法:在catch代码块中再次设置中断标识位为true,也就是二次调用才可以停止程序
5. 静态方法Thread.interrupted()
这个方法是用来判断线程是否被中断并清除当前中断状态
- 返回当前线程的中断状态,测试当前线程是否已被中断
- 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
public class InterruptedDemo {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println("----------------------1");
Thread.currentThread().interrupt();
System.out.println("----------------------2");
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
}
}
main线程默认的中断标志为false,第一次调用 Thread.interrupted()方法,返回当前的默认标志位也就是true,并清空当前中断状态(由于是false,所以清空也是false)
第二次同上,
在第三次之前,执行了Thread.currentThread().interrupt();,将中断标志位设置为true
第三次,首先返回当前的中断标志位true,然后清空当前中断标志位,也就是将中断标志位被设置为false
第四次的操作同第一次
6. LockSupport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
其中LockSupport的park()和unpark()的作用阻塞线程和解除阻塞线程
在线程中有三种方法实现等待唤醒机制
- 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 使用JUC包中的Condittion的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
其中LockSuport类使用了一种名为Permit(许可)的概念做到了阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
但是许可累加上限是1
LockSuport有两个方法是比较重要的
- park:用于线程获取许可证
- unpark:用于其他线程给某个线程发放许可证
当调用park方法时:
- 如果有凭证,则会直接消耗这个凭证然后正常退出
- 如果无凭证,就必须阻塞等待凭证可用
而unpark则相反,它会增加一个凭证,但凭证最多只能有一个,累加无效
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\tcome in");
//获取许可证
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t被唤醒");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t发出通知");
}, "t2").start();
}
}
注意:
- 三种实现线程等待和唤醒,前面两种等待和唤醒的顺序不能调换,必须现有等待,才可以唤醒;但是第三种LockSupport可以先发放许可证,等到这个线程用到了再获取许可证(这当中过程没有阻塞),这操作就有点像上高速的ETC通道,先把卡给了司机,司机要过关卡的时候就不需要等待,可以无畅通过
- 线程只能获取一个通行证,就上面案例而已,就算t2给t1发布了一万个凭证,这效果也和一个是一样的
6.1 面试题
为什么LockSupoort可以突破wait/notify原来的调用顺序
因为unpark获得一个凭证,之后再调用park方法,就可以名正言顺地凭证消费,故不会阻塞。先发放凭证后续畅通无阻
为什么唤醒两次阻塞两次,但最终结果还是会阻塞
因为凭证的数量最多是1,连续调用两次unpark和调用一次unpark效果一样的,只会增加一个凭证;而调用两次park则需要消耗两次凭证,凭证不够,故不能放行
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/94982.html