LockSupport与线程中断

4、LockSupport与线程中断

4.1 LockSupport

4.2 线程中断机制

4.3 从阿里蚂蚁金服面试题讲起

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

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

4.4 什么是中断机制?

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

4.5 中断的相关API方法之三大方法说明

4.6 通过一个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
public class Main {
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.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}

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

4.7 通过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
public class Main {
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.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}

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

4.8 通过Thread类自带的中断api实例方法实现

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 class Main {
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();
}
}

4.9 API

4.10 code

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 class Main {
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();
}
}

4.11 实例方法interrupt(),没有返回值

4.12 interrupt()源码分析

4.13 实例方法islnterrupted,返回布尔值

4.14 islnterrupted源码分析

4.15 API说明

  • 具体来说,当对一个线程,调用interrupt()时:
    • ①如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而己。被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。
    • ②如果线程处于被阻塞状态(例如处于sleep,wait,join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

4.16 code02

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
public class InterruptDemo2 {
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.getState());//TERMINATED
System.out.println("t1线程调用interrupt()后的的中断标识03:" + t1.isInterrupted());//????---false中断不活动的线程不会产生任何影响。
}
}

4.17 code02后手案例(重要,面试就是它,操蛋)

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

4.18 结论

  • sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在catch没有通过调用th.interrupt()方法再次将中断标识置为true,这就导致无限循环了。

4.19 静态方法Thread.interrupted()的说明

4.20 code

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InterruptDemo4 {
public static void main(String[] args) {
//测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个false。
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());
}
}

4.21 都会返回中断状态,两者对比

4.22 结论

  • 方法的注释也清晰的表达了“中断状态将会根据传入的Clearlnterrupted参数值确定是否重置”。
  • 所以,静态方法interrupted将会清除中断状态(传入的参数Clearlnterupted为true),实例方法islnterrupted则不会(传入的参数Clearlnterrupted为false)。

4.23 LockSupport是什么

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

  • 下面这句话,后面详细说:LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。

4.24 Object类中的wait和notify方法实现线程等待和唤醒之正常

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
public class Main {
public static void main(String[] args) {
Object objectLock = new Object();

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 ----被唤醒");
}
}, "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();
}
}

4.25 Object类中的wait和notify方法实现线程等待和唤醒之异常1

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
public class Main {
public static void main(String[] args) {
Object objectLock = new Object();

new Thread(() -> {
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(() -> {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t ----发出通知");
}, "t2").start();
}
}

4.26 Object类中的wait和notify方法实现线程等待和唤醒之异常2

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
public class Main {
public static void main(String[] args) {
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();

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

4.27 Condition接口中的await后signal方法实现线程的等待和唤醒之正常

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
public class Main {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread(() -> {
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();
}
}

4.28 Condition接口中的await后signal方法实现线程的等待和唤醒之异常1

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
public class Main {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread(() -> {
// 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();
}
}

4.29 Condition接口中的await后signal方法实现线程的等待和唤醒之异常2

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
public class Main {
public static void main(String[] args) {
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();

new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t ----发出通知");
} finally {
lock.unlock();
}
}, "t2").start();
}
}

4.30 LockSupport类中的park等待和unpark唤醒官网解释

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),但与Semaphore不同的是,许可的累加上限是1。

4.31 API

4.32 park()/park(Object blocker)

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

4.33 unpark(Thread thread)

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

4.34 正常+无锁块要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
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();
}
}

4.35 之前错误的先唤醒后等待,LockSupport照样支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} 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();

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

4.36 解释

  • 类似高速公路的ETC,提前买好了通行证unpark,到闸机处直接抬起栏杆放行了,没有park拦截了。

4.37 重点说明(重要)

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
  • LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程。
  • LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
  • 形象的理解:线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
  • 当调用park方法时:
    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
    • 如果无凭证,就必须阻塞等待凭证可用;
  • 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

4.38 面试题

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

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

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