欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
1. 线程中断机制
1.1 什么是中断?
- 首先一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend,Thread.resume 都已经被废弃了。
- 其次在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——
中断
。 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现
。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true
;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。- 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
1.2 中断的相关API方法
public void interrupt() | 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程 |
---|---|
public static boolean interrupted() |
静态方法 ,Thread.interrupted();判断线程是否被中断,并清除当前中断状态;这个方法做了两件事:①返回当前线程的中状态 ②将当前线程的中断状态设为false 。这个方法有点不好理解,因为连续调用两次的结果可能不一样。 |
public boolean isInterrupted() | 实例方法,判断当前线程是否被中断(通过检查中断标志位) |
1.3 如何使用中断标识停止线程?
在需要中断的线程中不断监听中断状态
,一旦发生中断,就执行相应的中断处理业务。
1.3.1 方法一:通过一个volatile变量实现
因为大家都知道该关键字具有内存可见性——即一个线程修改了主内存中的该变量的值,另一个线程就会立马知道,代码演示如下:
/**
* 通过一个volatile变量实现
*/
static volatile boolean isStop = false;
public static void m1() {
new Thread(() -> {
while(true) {
if(isStop) {
System.out.println("-----isStop = true,程序结束。");
break;
}
System.out.println("------hello isStop");
}
},"t1").start();
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
isStop = true;
},"t2").start();
}
复制代码
1.3.2 方法二:通过AtomicBoolean
代码演示如下:
/**
* 通过AtomicBoolean
*/
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void m2() {
new Thread(() -> {
while(true) {
if(atomicBoolean.get()) {
System.out.println("-----atomicBoolean.get() = true,程序结束。");
break;
}
System.out.println("------hello atomicBoolean");
}
},"t1").start();
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
atomicBoolean.set(true);
},"t2").start();
}
复制代码
1.3.3 方法三:通过Thread类自带的中断api方法实现
代码演示如下:
public static void m3()
{
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----isInterrupted() = true,程序结束。");
break;
}
System.out.println("------hello Interrupt");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
t1.interrupt();//修改t1线程的中断标志位为true
},"t2").start();
}
复制代码
方法说明:
-
实例方法
interrupt()
,没有返回值- public void interrupt(),实例方法,
调用interrupt()方法仅仅是当前线程中打了一个终止的标记位,并不是真正立刻停止线程
。 - 源码分析:
- public void interrupt(),实例方法,
-
实例方法
isInterrupted
,返回布尔值-
public boolean isInterrupted(),实例方法,获取中断标志位的当前值是什么,判断当前线程是否被中断(通过检查中断标志位),默认是false
-
源码分析:
-
1.4 当前线程的中断标识为true,是不是就立刻停止?
具体来说,当对一个线程,调用 interrupt() 时:
-
如果线程处于正常活动状态,那么会将
该线程的中断标志设置为 true,仅此而已
。被设置中断标志的线程将继续正常运行
,不受影响。所以interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。 -
如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
代码演示如下:
/**
*中断为true后,并不是立刻stop程序
*/
public static void m4() {
//中断为true后,并不是立刻stop程序
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 300; i++) {
System.out.println("------i: " + i);
}
System.out.println("t1.interrupt()调用之后02: "+Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
System.out.println("t1.interrupt()调用之前,t1线程的中断标识默认值: "+t1.isInterrupted());
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
t1.interrupt();
//活动状态,t1线程还在执行中
System.out.println("t1.interrupt()调用之后01: "+t1.isInterrupted());
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//非活动状态,t1线程不在执行中,已经结束执行了。
System.out.println("t1.interrupt()调用之后03: "+t1.isInterrupted());
}
复制代码
面试常问例子,代码如下:
public static void m5()
{
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----isInterrupted() = true,程序结束。");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();//??????? //线程的中断标志位为false,无法停下,需要再次掉interrupt()设置true
e.printStackTrace();
}
System.out.println("------hello Interrupt");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
t1.interrupt();//修改t1线程的中断标志位为true
},"t2").start();
}
复制代码
结论:
上述代码中如果没有在catch中将线程的中断状态再次设置为true
,则线程不会停止
,因为sleep
方法抛出异常之后,中断标识也被清空置为false
。
中断只是一个协同机制,修改中断标识位仅此而已,不是立刻stop打断
。
1.5 静态方法Thread.interrupted()
静态方法
,Thread.interrupted();- 判断线程是否被中断,并清除当前中断状态;这个方法做了两件事:
- ①
返回当前线程的中状态
- ②
将当前线程的中断状态设为false
。
- ①
- 这个方法有点不好理解,因为连续调用两次的结果可能不一样。
即该方法首先将当前线程的中断状态返回,接着将该线程的中断状态设置为false,代码演示如下:
// 作用是测试当前线程是否被中断(检查中断标志),返回一个 *boolean* 并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个 *false* 。
public static void main(String[] args)
{
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println("111111");
Thread.currentThread().interrupt();///----false---> true
System.out.println("222222");
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
}
复制代码
运行结果如下:
> main---false
main---false 111111 222222 main---true main---false
复制代码
静态方法跟实例方法都会返回中断状态,两者对比
综上所以,静态方法interrupted
将 会清除中断状态
(传入的参数ClearInterrupted为true
),实例方法isInterrupted则不会
(传入的参数ClearInterrupted为false)。
1.6 总结
线程中断相关的方法:
-
interrupt()方法是一个实例方法
- 它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。
-
isInterrupted()方法也是一个实例方法
- 它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
-
Thread类的静态方法interrupted()
- 返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
2. LockSupport
2.1 概念
LockSupport简介: LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,官网对于该类的解释如下图所示,LockSupport中的park()
和unpark()
的作用分别是阻塞线程和解除阻塞线程
。
3. 线程等待和唤醒方法
3.1 三种让线程等待和唤醒的方法
3.1.1 方式一:使用Object中的wait()方法让线程等待
使用Object中的notify()方法唤醒线程。 代码演示如下:
public class LockSupportDemo {
static Object objectLock=new Object();
public static void main(String[] args) {//main方法,一切程序的入口
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}, "A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
}, "B").start();
}
}
复制代码
运行结果如下:
> A ------come in
B ------通知 A ------被唤醒
复制代码
特殊情况:如果先通知,再阻塞呢? 修改上述的代码如下:
public class LockSupportDemo {
static Object objectLock=new Object();
public static void main(String[] args) {//main方法,一切程序的入口
new Thread(() -> {
//为了让B线程先通知,A线程再阻塞
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}, "A").start();
new Thread(() -> {
synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
}, "B").start();
}
}
复制代码
运行结果如下:
> B ------通知
A ------come in //A线程一直在阻塞,等待唤醒
复制代码
小总结 :
wait和notify方法必须要在同步块或者方法里面且成对出现使用
先wait后notify才OK
3.1.2 方式二:使用JUC包中Condition
使用JUC包中Condition的await()
方法让线程等待,使用signal()
方法唤醒线程 。 传统的synchronized和Lock实现等待唤醒通知的约束:
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
3.1.3 方式三:LockSupport类
可以阻塞当前线程以及唤醒指定被阻塞的线程。 通过park()
和unpark(thread)
方法来实现阻塞和唤醒线程
的操作,LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)
的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零
,默认是零
。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
主要方法 :
-
阻塞:
park()/park(Object blocker)
阻塞当前线程/阻塞传入的具体线程permit默认是0
,所以一开始调用park()方法
,当前线程就会阻塞
,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒
, 然后会将permit再次设置为O并返回。 -
唤醒:
unpark(Thread thread)
唤醒处于阻断状态的指定线程
调用unpark(thread)方法
后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)
会自动唤醒thread线程, 即之前阻塞中的LockSupport.park()方法会立即返回
。
代码演示1:正常+无锁块要求
public class LockSupportDemo1 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
LockSupport.park();//被阻塞......等待许可证
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}, "A");
a.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread b = new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"\t"+"------唤醒线程a");
}, "B");
b.start();
}
}
复制代码
运行结果如下:
> A ------come in
B ------唤醒线程a A ------被唤醒
复制代码
代码演示2:之前错误的先唤醒后等待,LockSupport照样支持
public class LockSupportDemo2 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
LockSupport.park();//被阻塞......等待许可证
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}, "A");
a.start();
Thread b = new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"\t"+"------唤醒线程a");
}, "B");
b.start();
}
}
复制代码
运行结果如下:
> B ------唤醒线程a
A ------come in A ------被唤醒
复制代码
sleep方法3秒后醒来,执行park方法无效,没有阻塞效果,解释如下:先执行了unpark(t1)导致上面的park方法形同虚设无效,时间一样
3.2 重点说明
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语 LockSupport是一个线程阻塞工具类
,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。
归根结底,LockSupport调用的Unsafe中的native代码
。 LockSupport提供park()
和unpark()
方法实现阻塞线程和解除线程阻塞的过程。 LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0, 调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。 每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
形象的理解:线程阻塞需要消耗凭证(permit),这个凭证最多只有1个
。
当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
而unpark
则相反,它会增加一个凭证
,但凭证最多只能有1个,累加无效
。
面试题
- 为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
- 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1
,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证; 而调用两次park却需要消费两个凭证,证不够,不能放行。
参考资料
Java并发编程知识体系
Java并发编程的艺术
Java多线程编程核心技术
Java并发实现原理 JDK源码剖析