JUC(8) LockSupport与线程中断
2024-02-05 00:03:15 # Language # Java

1. 线程中断机制

面试题:

  • Java.lang.Thread下的三个方法的用法和区别

img

  • 如何中断一个运行中的线程?
  • 如何停止一个运行中的线程?

1.1 什么是中断机制

  • 首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,所以Thread.stop, Thread.suspend, Thread.resume都已经被废弃了
  • 其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制
    • 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自行实现。若要中断一个线程,你需要手动调用该线程 interrupt 方法,该方法也仅仅是将该线程对象的中断标识设置为true,接着你需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟应该做什么需要你自己写代码实现
    • 每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断
    • 通过调用线程对象的 interrupt 方法将该线程的标识位设置为true;可以在别的线程中调用,也可以在自己的线程中调用

1.2 中断API

  • public void interrupt()

    • 实例方法 Just to set the interrupt flag

    • 实例方法仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程

  • public static boolean interrupted()

    • 静态方法 Thread.interrupted();

    • 判断当前线程是否被中断并清除当前中断状态(做了两件事情)

      • 返回当前线程的中断状态,测试当前线程是否已被中断

      • 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态

    • 如果连续两次调用此方法,则第二次返回false

  • public boolean isInterrupted()

    • 实例方法

    • 判断当前线程是否被中断(通过检查中断标志位)

1.3 面试题中断机制考点

如何停止运行中的线程?

  • 通过一个volatile变量实现
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
static volatile boolean isStop = false; //volatile表示的变量具有可见性

public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println(Thread.currentThread().getName() + " isStop的值被改为true,t1程序停止");
break;
}
System.out.println("-----------hello volatile");
}
}, "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> isStop = true, "t2").start();
}
/**
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* -----------hello volatile
* t1 isStop的值被改为true,t1程序停止
*/
  • 通过AtomicBoolean
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
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() + " atomicBoolean的值被改为true,t1程序停止");
break;
}
System.out.println("-----------hello atomicBoolean");
}
}, "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> atomicBoolean.set(true), "t2").start();
}

/**
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* -----------hello atomicBoolean
* t1 atomicBoolean的值被改为true,t1程序停止
*/
  • 通过Thread类自带的中断api实例方法实现
    • 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。
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
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " isInterrupted()的值被改为true,t1程序停止");
break;
}
System.out.println("-----------hello isInterrupted()");
}
}, "t1");
t1.start();

try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

//t2向t1放出协商,将t1中的中断标识位设为true,希望t1停下来
new Thread(() -> t1.interrupt(), "t2").start();
}
/**
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* -----------hello isInterrupted()
* t1 isInterrupted()的值被改为true,t1程序停止
*/

当前线程中断标识为true,是不是线程就立刻停止

答案是不立刻停止,具体来说,当对一个线程调用interrupt时:

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响
  • 如果线程处于阻塞状态(例如sleep, wait, join等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断标志也将被清除),并抛出一个InterruptedException异常。
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
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " 中断标志位为:true 程序停止");
break;
}
//sleep方法抛出InterruptedException后,中断标识也被清空置为false,如果没有在
//catch方法中必须调用interrupt方法再次将中断标识置为true,否则将导致无限循环
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();
}

静态方法Thread.interrupted(), 谈谈你的理解

1
2
3
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
  • 返回当前线程中断标志位,并且清空中断标志位
    • 也就是说,如果连续两次调用该方法,第二次调用将返回false
    • 除非当前线程在第一次和第二次调用之间再次被 interrupt

2. 线程等待唤醒机制

三种实现方式:

  1. 关键字 synchronizedwait()/notify() 一起使用可以实现等待/通知模式
  2. Lock 接口中的 newContition() 方法返回 Condition 对象,Condition 类使用 await()/signal() 也可以实现等待/通知模式
  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

2.1 synchronized实现

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
class Share{
// 设置临界资源
private int number = 0;

// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
while (number != 0) {
// number不为0,等待
// wait 有一个特点,在哪里睡,就在哪里醒
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number);
// 唤醒其他线程
this.notifyAll();
}

// 实现-1操作
public synchronized void decr() throws InterruptedException {
while (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}

注意

上述代码中进行判断时必须使用 while 而不是 if,否则会产生虚假唤醒问题,即唤醒后不需要再进行判断而可以直接执行

2.2 Lock实现

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
class Share {
// 设置临界资源
private int number = 0;
// 创建一个Condition
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

// 实现+1操作
public void incr() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.print(Thread.currentThread().getName() + "::" + number + "--->");
condition.signalAll();
} finally {
lock.unlock();
}
}

// 实现-1操作
public void decr() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}

定制化通信

案例:启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次,一共进行10轮

具体思路:每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程。创建一个可重入锁与三个开锁通知

该案例被称为单标志法

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
class Share{
private int flag = 1;

private Lock lock = new ReentrantLock();
// 创建三个Comdition对象,为了定向唤醒相乘
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();

public void Aprint(int loop) throws InterruptedException {
lock.lock();
try{
while(flag!=1) {
c1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 2; //修改标志位,定向唤醒 线程b
c2.signal();
} finally {
lock.unlock();
}
}
public void Bprint(int loop) throws InterruptedException {
lock.lock();
try{
while(flag!=2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 3; //修改标志位,定向唤醒 线程c
c3.signal();
} finally {
lock.unlock();
}
}

public void Cprint(int loop) throws InterruptedException {
lock.lock();
try{
while(flag!=3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 1; //修改标志位,定向唤醒 线程a
c1.signal();
} finally {
lock.unlock();
}
}
}

单标志法的问题

  • 该算法可确保每次只允许一个进程进入临界区,但进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区

2.3 LockSupport实现

上述两种方法使用都有限制

  • 线程需要先获得并持有锁,必须在锁块(synchronized 或 lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其中的 park()unpark() 的作用分别是阻塞线程和解除阻塞线程

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

主要方法(全部都是静态方法, 底层使用 UNSAFE 类实现)

  • 阻塞:
    • Permit许可证默认没有不能放行,所以一开始调 park() 方法当前线程就会阻塞,直到别的线程给当前线程的发放 Permit,park 方法才会被唤醒。
    • park(): 阻塞当前线程
  • 唤醒:
    • 调用 unpark(thread) 方法后,就会发放 thread 线程的许可证,会自动唤醒 park 线程,即之前阻塞中的 LockSuppot.park() 方法会立即返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
/*
* t1 ----------come in
* t2 ----------发出通知
* t1 ----------被唤醒
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----------come in");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");
}, "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();
}

3. 总结

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞后也有对应的唤醒方法。归根结底,LockSupport时调用Unsafe中的native代码
  • LockSupport提供 park()unpark() 方法实现阻塞线程和解除线程阻塞的过程,LockSupport和每个使用它的线程都有一个许可(Permit)关联,每个线程都有一个相关的permit,permit最多只有一个,重复调用 unpark 也不会积累凭证。
  • 形象理解:线程阻塞需要消耗凭证(Permit),这个凭证最多只有一个

  • 当调用park时,如果有凭证,则会直接消耗掉这个凭证然后正常退出。如果没有凭证,则必须阻塞等待凭证可用;

  • 当调用unpark时,它会增加一个凭证,但凭证最多只能有1个,累加无效。

面试题

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

    • 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞,先发放了凭证后续可以畅通无阻。
  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

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