Java——聊聊JUC中的线程中断机制 & LockSupport
Posted 宋宋_浩浩_Java工程师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——聊聊JUC中的线程中断机制 & LockSupport相关的知识,希望对你有一定的参考价值。
文章目录:
3.3 当前线程的中断标识为true,是不是线程就立刻停止?
3.5 静态方法public static boolean interrupted()
1.什么是中断机制?
- 首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
- 其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制——中断。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
借尚硅谷周阳老师的例子:顾客在无烟餐厅中吸烟,服务员希望他别吸烟了,不是强行停止他吸烟,而是给他的标志位打为true,具体的停止吸烟还是要顾客自己停止。(体现了协商机制)
- 中断相关的三大API方法如下图:↓↓↓
2.如何停止中断运行中的线程?
2.1 通过一个volatile变量实现
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo1
static volatile boolean isStop = false;
public static void main(String[] args)
new Thread(() ->
while (true)
if (isStop)
System.out.println(Thread.currentThread().getName() + " isStop被修改为true,线程停止");
break;
System.out.println(Thread.currentThread().getName() + " hello volatile....");
, "t1").start();
try
TimeUnit.MILLISECONDS.sleep(20);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
isStop = true;
, "t2").start();
2.2 通过AtomicBoolean原子布尔类
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class InterruptDemo2
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() + " isStop被修改为true,线程停止");
break;
System.out.println(Thread.currentThread().getName() + " hello AtomicBoolean....");
, "t1").start();
try
TimeUnit.MILLISECONDS.sleep(20);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
atomicBoolean.set(true);
, "t2").start();
2.3 通过Thread类自带的中断API方法实现
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo3
public static void main(String[] args)
Thread t1 = new Thread(() ->
while (true)
if (Thread.currentThread().isInterrupted())
System.out.println(Thread.currentThread().getName() + " isStop被修改为true,线程停止");
break;
System.out.println(Thread.currentThread().getName() + " hello isInterrupted....");
, "t1");
t1.start();
try
TimeUnit.MILLISECONDS.sleep(20);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
t1.interrupt();
, "t2").start();
3.Thread类的三大API说明
3.1 实例方法interrupt(),没有返回值
这个interrupt()实例方法,底层实际上调用了interrupt0()这个方法,根据后面的注释可以看到,仅仅是设置中断标识位,而interrupt0这个方法是一个native方法,底层又调用了C。
而在jdk官方文档中可以看到有关这个方法的叙述。
3.2 实例方法isInterrupted(),返回布尔值
这个实例方法的底层调用了一个native方法,传入了一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。
具体来说,当对一个线程,调用 interrupt() 时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。
(中断不活动的线程不会产生任何影响,看下面案例)
3.3 当前线程的中断标识为true,是不是线程就立刻停止?
否,仅仅设置了一个中断状态
看看中断是否会立即停止这个300的线程。否,虽然中断标志位变了。但是i一直输完300次,才最终停止。
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo4
public static void main(String[] args)
Thread t1 = new Thread(() ->
for (int i = 0; i < 300; i++)
System.out.println("--------- " + i);
System.out.println("t1调用interrupt()之后的中断标识02---- " + Thread.currentThread().isInterrupted());
, "t1");
t1.start();
System.out.println("t1线程默认的中断标识---- " + t1.isInterrupted());
try
TimeUnit.MILLISECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
t1.interrupt();
System.out.println("t1调用interrupt()之后的中断标识01---- " + t1.isInterrupted());
对上面的代码稍作改变,如下:↓↓↓
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo5
public static void main(String[] args)
Thread t1 = new Thread(() ->
for (int i = 0; i < 300; i++)
System.out.println("--------- " + i);
System.out.println("after t1.interrupt()---第2次---- " + Thread.currentThread().isInterrupted());
, "t1");
t1.start();
System.out.println("before t1.interrupt()---- " + t1.isInterrupted());
try
TimeUnit.MILLISECONDS.sleep(2);
catch (InterruptedException e)
e.printStackTrace();
t1.interrupt();
System.out.println("after t1.interrupt()---第1次--- " + t1.isInterrupted());
try
TimeUnit.MILLISECONDS.sleep(2000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("after t1.interrupt()---第3次--- " + t1.isInterrupted());
在输出结果中,我们可以看到和我们预想的都一样,只有最后一行输出,t1线程它自己不是已经打断了吗?那中断标识就应该是 true 啊?为什么变成了false???
原因是上面的代码中,t1线程打印300次i,而最后一行输出代码是在2000ms之后的,t1线程是完全可以在这个时间内完成300次i的打印工作,所以程序运行到最后一行输出,t1线程已经结束死亡了,再根据 interrupt 方法api中的这句话:
中断不存在的线程不需要任何效果。
我们就懂了,中断不存在的线程没什么意义的,所以这里的中断标识自然就恢复成了默认值 false。
3.4 在3.3中断程序的基础上,添加sleep睡眠
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo6
public static void main(String[] args)
Thread t1 = new Thread(() ->
while (true)
if (Thread.currentThread().isInterrupted())
System.out.println(Thread.currentThread().getName() + " 中断标识位:" +
Thread.currentThread().isInterrupted() + " 线程终止....");
break;
try
Thread.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("---- hello InterruptDemo6");
, "t1");
t1.start();
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
t1.interrupt();
, "t2").start();
这个程序是停不下来的,我是不想耗费太多CPU资源,手动停止了。
原因就是:
如果该线程阻塞的调用
wait()
,wait(long)
,或wait(long, int)
的方法Object
类,或者在join()
,join(long)
,join(long, int)
,sleep(long)
,或sleep(long, int)
,这个类的方法,那么它的中断状态将被清除,并且将收到一个InterruptedException
。所以这个时候线程t1的中断标识位被清除,恢复成了false,那么久永远也停不下来了。
如何修改上面的代码,使得程序正常运行停止呢? →
- 中断标志位 默认是false。
- t2 ----->t1发出了中断协商,t2调用t1.interrupt(),中断标志位true。
- 中断标志位true,正常情况下,程序停止。
- 中断标志位true,异常情况下,InterruptedException,将会把中断状态清除,并且将收到InterruptedException。中断标志位false导致无限循环。
- 在catch块中,需要再次给中断标志位设置为true,2次调用停止。
package com.szh.demo.interrupt;
import java.util.concurrent.TimeUnit;
public class InterruptDemo6
public static void main(String[] args)
Thread t1 = new Thread(() ->
while (true)
if (Thread.currentThread().isInterrupted())
System.out.println(Thread.currentThread().getName() + " 中断标识位:" +
Thread.currentThread().isInterrupted() + " 线程终止....");
break;
try
Thread.sleep(200);
catch (InterruptedException e)
Thread.currentThread().interrupt(); //关键代码
e.printStackTrace();
System.out.println("---- hello InterruptDemo6");
, "t1");
t1.start();
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
t1.interrupt();
, "t2").start();
3.5 静态方法public static boolean interrupted()
静态方法,
Thread.interrupted();
判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态 2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
package com.szh.demo.interrupt;
public class InterruptDemo7
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();//中断标志位设置为true
System.out.println("-----2");
System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
System.out.println(Thread.currentThread().getName() + "\\t" + Thread.interrupted());
前两次调用没啥说的,因为main主线程并没有中断,第三次调用的时候,因为上面已经 interrupt 了,所以被中断了,这里中断标识位肯定就是 true。此时这个静态方法在中断之后第一次调用(返回当前线程的中断状态,被中断了就是true;第二件事,将当前线程的中断标识重置为false)。所以当最后一行再次调用它的时候,就是false了。
看一下这个静态方法的源码:↓↓↓ 在那个isInterrupt实例方法中传入的 一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。
这两个方法在底层都调用了native方法isInterrupted。 只不过传入参数ClearInterrupted一个传参传了true,一个传了false。
静态方法interrupted() 中true表示清空当前中断状态。 实例方法isInterrupted 则不会。
4.LockSupport
用于创建锁和其他同步类的基本线程阻塞原语。
这个类与每个使用它的线程相关联,一个许可证(在
Semaphore
类的意义上)。 如果许可证可用,则呼叫park
将park
返回,在此过程中消耗它; 否则可能会阻止。 致电unpark
使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
核心就是
park()
和unpark()
方法
park()
方法是阻塞线程
unpark()
方法是解除阻塞线程
4.1 线程的等待唤醒机制
使用Object中的
wait()
方法让线程等待,使用Object中的notify()
方法唤醒线程。(有局限性)使用JUC包中
Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程。(有局限性)
LockSupport
类可以阻塞当前线程以及唤醒指定被阻塞的线程。
4.2 wait、notify
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
public class LockSupportDemo1
public static void main(String[] args)
final Object obj = new Object();
new Thread(() ->
synchronized (obj)
System.out.println(Thread.currentThread().getName() + " --- come in");
try
obj.wait();
catch (InterruptedException e)
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
, "t1").start();
try
TimeUnit.SECONDS.sleep(3L);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
synchronized (obj)
obj.notify();
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
, "t2").start();
异常情况1:将 synchronized 同步代码块对应的代码注释掉。
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
public class LockSupportDemo1
public static void main(String[] args)
final Object obj = new Object();
new Thread(() ->
//synchronized (obj)
System.out.println(Thread.currentThread().getName() + " --- come in");
try
obj.wait();
catch (InterruptedException e)
e.printStackTrace();
//
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
, "t1").start();
try
TimeUnit.SECONDS.sleep(3L);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
//synchronized (obj)
obj.notify();
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
//
, "t2").start();
异常情况2:将wait和notify顺序调换。
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
public class LockSupportDemo1
public static void main(String[] args)
final Object obj = new Object();
new Thread(() ->
try
TimeUnit.SECONDS.sleep(3L);
catch (InterruptedException e)
e.printStackTrace();
synchronized (obj)
System.out.println(Thread.currentThread().getName() + " --- come in");
try
obj.wait();
catch (InterruptedException e)
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
, "t1").start();
// try
// TimeUnit.SECONDS.sleep(3L);
// catch (InterruptedException e)
// e.printStackTrace();
//
new Thread(() ->
synchronized (obj)
obj.notify();
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
, "t2").start();
小总结
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒。要保证先wait,后notify才OK。
wait和notify方法必须要在同步块或者方法里面,且成对出现使用。
4.3 await、signal
package com.szh.demo.locksupport;
import javax.swing.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockSupportDemo2
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args)
new Thread(() ->
lock.lock();
try
System.out.println(Thread.currentThread().getName() + " --- come in");
condition.await();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
catch (InterruptedException e)
e.printStackTrace();
finally
lock.unlock();
, "t1").start();
try
TimeUnit.SECONDS.sleep(1L);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
lock.lock();
try
condition.signal();
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
finally
lock.unlock();
, "t2").start();
异常情况1:将对应的加锁解锁的代码注释掉,报错信息和第一个案例是一样的。
异常情况2:先进行 signal,再进行 await,报错信息和第一个案例是一样的。
小总结
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒。一定要先await后signal,不能反了
Condition中的线程等待和唤醒方法,需要先获取锁
4.4 park、unpark
调用
LockSupport.park()
时,发现它调用了unsafe类
,并且默认传了一个0。permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。
调用
LockSupport.unpark();
时,也调用了unsafe类。
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
解决上面两个案例的第一个问题:必须放在锁块中,LockSupport不需要这样做。
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo3
public static void main(String[] args)
Thread t1 = new Thread(() ->
System.out.println(Thread.currentThread().getName() + " --- come in");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了");
, "t1");
t1.start();
try
TimeUnit.SECONDS.sleep(2L);
catch (InterruptedException e)
e.printStackTrace();
new Thread(() ->
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
, "t2").start();
解决上面两个案例的第一个问题:必须先等待,后唤醒,LockSupport不需要这样做,先唤醒后等待照样OK。
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo3
public static void main(String[] args)
Thread t1 = new Thread(() ->
try
TimeUnit.SECONDS.sleep(3L);
catch (InterruptedException e)
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了 " + System.currentTimeMillis());
, "t1");
t1.start();
new Thread(() ->
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
, "t2").start();
这里会先执行t2线程的unpark方法,此时t1线程手中就有了一张许可证,当t1线程睡眠3秒之后,执行代码,走到park方法不会再阻塞,直接拿出许可证,继续向下执行,所以看代码的花费时间就知道,这里的park是无效没有阻塞的。
jdk官方文档中说了,与信号量不同,许可证不能累积,最多只有一个。
老子就不信这个邪,我非得给你来两个许可证,看看下面的代码。
package com.szh.demo.locksupport;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo3
public static void main(String[] args)
Thread t1 = new Thread(() ->
try
TimeUnit.SECONDS.sleep(3L);
catch (InterruptedException e)
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " --- come in " + System.currentTimeMillis());
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " --- 被唤醒了 " + System.currentTimeMillis());
, "t1");
t1.start();
new Thread(() ->
LockSupport.unpark(t1);
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " --- 发出通知");
, "t2").start();
可以看到,代码卡在这里了,这是因为你虽然发了两个许可证,但是最多只能持有一个,那么当第二次park尝试再去获取许可证时,已经不可能了,因为t1线程手中的那个许可证已经被第一次park的时候消费掉了。
当调用park方法时如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用。
而unpark则相反, 它会增加一个凭证, 但凭证最多只能有1个, 累加无效。
针对park和unpark方法的代码实测结论:
- park:unpark = 1:1,代码正常执行无误。
- park:unpark = 1:n,代码正常执行无误。(尽管unpark了多次,但是当前线程最多只能持有1个许可证,之后也只park了一次,消费了一个许可证,所以没问题,但还是不推荐这样写)
- park:unpark = n:1,代码卡死无法结束。(当前线程最多只能持有1个许可证,park一次消费一个,park多次直接无证,当前线程无法正常结束)
- park:unpark = n:n,代码卡死无法结束。(原因在上面说过了)
以上是关于Java——聊聊JUC中的线程中断机制 & LockSupport的主要内容,如果未能解决你的问题,请参考以下文章
Java——聊聊JUC中的线程中断机制 & LockSupport
Java——聊聊JUC中的线程中断机制 & LockSupport
Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)
Java——聊聊JUC中的锁(synchronized & Lock & ReentrantLock)