JUC - 线程中断与线程等待唤醒(LockSupport)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC - 线程中断与线程等待唤醒(LockSupport)相关的知识,希望对你有一定的参考价值。


中断机制

什么是中断机制?

​ 首先
​ 一个线程不应该由其他线程来强制中断或停止,而是应该有线程自己自行停止,自己来决定自己的命运。
​ 所以,Thread.stop, Thead.suspend, Thead.resumer都已经被废弃了。

​ 其次
​ 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
​ 因此,Java提供了一种用于停止线程的协商机制–中断,即中断标识协商机制。

中断只是一种协商协作机制,Java中没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断表示设置成true
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断

每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识为设为true; 可以在别的线程中调用,也可以在自己的线程中调用。


JUC

  1. 通过volatile变量实现
  2. 通过AtomicBoolean实现
  3. 通过Thread类自带的中断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 interrupt api");

, "t1");
t1.start();

System.out.println("-----t1的默认中断标志位:"+t1.isInterrupted());

//暂停毫秒
try TimeUnit.MILLISECONDS.sleep(20); catch (InterruptedException e) e.printStackTrace();

//t2向t1发出协商,将t1的中断标志位设为true希望t1停下来
new Thread(() ->
t1.interrupt();
,"t2").start();
//t1.interrupt();



static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
private static void m2_atomicBoolean()

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.MILLISECONDS.sleep(20); catch (InterruptedException e) e.printStackTrace();

new Thread(() ->
atomicBoolean.set(true);
,"t2").start();


static volatile boolean isStop = false;
private static void m1_volatile()

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.MILLISECONDS.sleep(20); catch (InterruptedException e) e.printStackTrace();

new Thread(() ->
isStop = true;
,"t2").start();

总结:当对一个线程,调用interrupt方法时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true, 仅此而已。
被设置中断标志的线程将继续正常运行,不受影响
所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。

②如果线程处于阻塞状态(例如处理sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个interruptedException异常。

public static void main(String[] args)

//实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
Thread t1 = new Thread(() ->
for (int i = 1; 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());//false

//暂停毫秒
try TimeUnit.MILLISECONDS.sleep(2); catch (InterruptedException e) e.printStackTrace();
t1.interrupt();//true
System.out.println("t1线程调用interrupt()后的的中断标识01:"+t1.isInterrupted());//true

try TimeUnit.MILLISECONDS.sleep(2000); catch (InterruptedException e) e.printStackTrace();
System.out.println("t1线程调用interrupt()后的的中断标识03:"+t1.isInterrupted());
//????---false中断不活动的线程不会产生任何影响。
//ps: 两秒后线程已经执行完

中断协商案例深度解析

public static void main(String[] args)

Thread t1 = new Thread(() ->
while (true)

if(Thread.currentThread().isInterrupted())

System.out.println(Thread.currentThread().getName()+"\\t " +
"中断标志位:"+Thread.currentThread().isInterrupted()+" 程序停止");
break;


try
Thread.sleep(200);
catch (InterruptedException e)
// Thread.currentThread().interrupt();//没有它,程序不会停止,为什么要在异常处,再调用一次??
e.printStackTrace();


System.out.println("-----hello InterruptDemo3");

, "t1");
t1.start();

//暂停几秒钟线程
try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();

new Thread(() -> t1.interrupt(),"t2").start();


/**
* 1 中断标志位,默认false
* 2 t2 ----> t1发出了中断协商,t2调用t1.interrupt(),中断标志位true
* 3 中断标志位true,正常情况,程序停止,^_^
* 4 中断标志位true,异常情况,InterruptedException,将会把中断状态将被清除,并且将收到InterruptedException 。中断标志位false
* 导致无限循环
*
* 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止程序才OK
*/

​官方描述​

JUC

中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断

public static void main(String[] args)

//测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个false。


System.out.println(Thread.currentThread().getName()+"\\t"+Thread.interrupted()); //false
System.out.println(Thread.currentThread().getName()+"\\t"+Thread.interrupted());//false
System.out.println("----1");
Thread.currentThread().interrupt();// 中断标志位设置为true
System.out.println("----2");
System.out.println(Thread.currentThread().getName()+"\\t"+Thread.interrupted());//true
System.out.println(Thread.currentThread().getName()+"\\t"+Thread.interrupted());//false

LockSupport.park();

Thread.interrupted();//静态方法

Thread.currentThread().isInterrupted();//实例方法

静态方法interrupted将会清楚中断状态(源码传入的ClearInterrupted为true)

实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)

总结

线程中断相关的方法:

public void interrupt(); interrupt()方法是一个实例方法
它通知目标线程中断,也仅仅是设置目标线程的中断标志位为true.

public boolean isInterrupted(); isInterrupted()方法也是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志

public static boolean interrupted(), Thread类的静态方法interrupted()
返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false, 此方法调用之后会清除当前线程的中断标志位的状态(将中断标志位置为false了),返回当前值并清零置false

线程等待和唤醒

LockSupport是用来创建和其他同步类的基本线程阻塞原语

​文档​

JUC

LockSupport中的 park()unpark() 的作用分别是阻塞线程和解除被阻塞的线程

三种线程等待唤醒的方式

  1. 使用Object的wait()方法让线程等待,使用 Object中的notify()方法唤醒线程
  2. 使用JUC包中Condition的await方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object

private static void syncWaitNotify()

Object objectLock = new Object();

new Thread(() ->
try TimeUnit.SECONDS.sleep(1); 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 ----被唤醒");

,"t1").start();

//暂停几秒钟线程
try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();

new Thread(() ->
synchronized (objectLock)
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\\t ----发出通知");

,"t2").start();

wait()和notify()必须放在同步代码块或同步方法中,并且成对出现
必须现wait()在notify(),否则程序无法执行,无法唤醒


Condition

private static void lockAwaitSignal()

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread(() ->
try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();
lock.lock();
try

System.out.println(Thread.currentThread().getName()+"\\t ----come in");
condition.await();
System.out.println(Thread.currentThread().getName()+"\\t ----被唤醒");
catch (InterruptedException e)
e.printStackTrace();
finally
lock.unlock();

,"t1").start();

//暂停几秒钟线程
try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();

new Thread(() ->
lock.lock();
try

condition.signal();
System.out.println(Thread.currentThread().getName()+"\\t ----发出通知");
finally
lock.unlock();

,"t2").start();

Condtion中的线程等待和唤醒方法,需要先获取锁
一定要先await再signal


LockSupport

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
但与Semaphores不同,许可证不会累积,最多只有一个

park()/park(Object blocker):阻塞,permit许可证默认没有不能方向,所以一开始调用park()方法当前线程就会阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒

unpark(Thread thread):唤醒,调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回

public static void main(String[] args)

Thread t1 = new Thread(() ->
try TimeUnit.SECONDS.sleep(3); catch (InterruptedException e) e.printStackTrace();
System.out.println(Thread.currentThread().getName() + "\\t ----come in"+System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\\t ----被唤醒"+System.currentTimeMillis());
, "t1");
t1.start();

//暂停几秒钟线程
//try TimeUnit.SECONDS.sleep(1); catch (InterruptedException e) e.printStackTrace();

new Thread(() ->
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"\\t ----发出通知");
,"t2").start();

LockSupport 天生无锁块要求
之前错误的先唤醒后后等待,LockSupport照样支持,先unpark再park相当于提前有了通行证unpark,park时就没有拦截。park和unpark必须一一对应,因为许可证不会累积,最多只有一个

总结

LockSupport是一个线程阻塞工具类,所有的方法都是静态的,可以让线程在任意位置阻塞,阻塞之后也有对于的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码(native标识的方法即调用底层C++、C代码)。

LockSupport提供的park() 和 unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会累加凭证。

理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。

当调用park方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
  • 如果无凭证,就必须阻塞等待凭证可用;

而unpark则相反,它会增加一个凭证,但凭证最多只能有一个,累加无效。

为什么可以突破wait/notify的原有调用顺序?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的靠凭证消费,故不会阻塞。
先发放了凭证后续可以畅通无阻。

为什么唤醒两次后阻塞两次,但最终结果还是会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够不能放行。


以上是关于JUC - 线程中断与线程等待唤醒(LockSupport)的主要内容,如果未能解决你的问题,请参考以下文章

JUC-三种等待唤醒方式

Java多线程与并发编程——ConditionCallable&Future

Java多线程与并发编程——ConditionCallable&Future

Java多线程与并发编程——ConditionCallable&Future

JUC多线程:线程的中断停止

JUC多线程:线程的中断停止