6. 线程中断与 LockSupport
6.1 线程中断机制
大厂(蚂蚁金服)面试题:
什么是中断?
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop
, Thread.suspend
, Thread.resume
都已经被废弃了。
其次,在 Java 中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java 提供了一种用于停止线程的机制——中断。
**中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。**若要中断一个线程,你需要手动调用该线程的 interrupt
方法,**该方法也仅仅是将线程对象的中断标识设成 true **;接着你需要自己写代码不断地检测当前线程的标识位,如果为 true ,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为 true 表示中断,为 false 表示未中断;通过调用线程对象的interrupt
方法将该线程的标识位设为 true;可以在别的线程中调用,也可以在自己的线程中调用。
中断的相关 API 方法
如何使用中断标识停止线程?
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。
方法:
- 通过一个
volatile
变量实现
private static volatile boolean isStop = false;
public static void main(String[] args) {
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();
}
- 通过
AtomicBoolean
private static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
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();
}
- 通过
Thread
类自带的中断 api 方法实现
实例方法interrupt()
,没有返回值
源码:
public void interrupt() {
if (this != Thread.currentThread()) {
checkAccess();
// thread may be blocked in an I/O operation
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupted = true;
interrupt0(); // inform VM of interrupt 这里调用 OS 底层,即 调用 C++
b.interrupt(this);
return;
}
}
}
interrupted = true;
// inform VM of interrupt
interrupt0();
}
实例方法isInterrupted()
,返回布尔值
public static void main(String[] args) {
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();
}
// 修改t1线程的中断标志位为true
new Thread(t1::interrupt, "t2").start();
}
当前线程的中断标识为true,是不是就立刻停止(重点)?
我们首先来继续说下 interrupt()
方法
当对一个线程,调用 interrupt()
时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
- 如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),在别的线程中调用当前线程对象的 interrupt 方法,那么线程将立即退出被阻塞状态,并抛出一个
InterruptedException
异常。
证明代码:
public static void main(String[] args) {
// 中断为true后,并不是立刻stop程序
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 600; 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());
}
// 结果
------i: 1
------i: 2
------i: 3
t1.interrupt()调用之前,t1线程的中断标识默认值: false
...
------i: 445
t1.interrupt()调用之后01: true // 从这里已经可以看出 当我们将 t1 线程 interrupt 之后,for 循环还在继续,说明线程没有停止 此时 t1 还处于活动状态 此时的 isInterrupted 为 true
------i: 446
...
------i: 600
t1.interrupt()调用之后02: true // 调用 interrupt 之后, 线程中断状态位为 true
t1.interrupt()调用之后03: false // 这里为 false 是因为 线程已经结束,停止了,此时应恢复原始状态 false (JDK17不同)
代码继续,问题发现:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----isInterrupted() = true,程序结束。");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------hello Interrupt");
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(t1::interrupt, "t2").start();
}
// 此时运行程序后会发现 程序会报 InterruptedException 异常,并且 程序无法停止 继续打印 ------hello Interrupt
why???
我们上面看了 interrupt
方法的源码,在注释中解释了,当中断 wait、join、sleep等阻塞方法时,该线程的中断标识将被清除设置为 false,并且抛出 InterruptedException 异常,并且在 sleep 的源码中也有相应的解释
如何改进(重点):
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----isInterrupted() = true,程序结束。");
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// 线程的中断标志位为false,无法停下,需要再次掉interrupt()设置true
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("------hello Interrupt");
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(t1::interrupt, "t2").start();
}
// 我们在 catch 中再次将 中断标识 设置为 true 这样就不会导致无限循环了
总结:中断只是一种协同机制,修改中断标识位仅此而已,不是立刻 stop 打断
静态方法Thread.interrupted()
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
和普通方法对比
方法的注释也清晰的表达了“中断状态将会根据传入的 ClearInterrupted
参数值确定是否重置”。
所以,静态方法 interrupted
将会清除中断状态(传入的参数 ClearInterrupted
为 true
),实例方法 isInterrupted
则不会(传入的参数 ClearInterrupted
为 false
)
总结线程中断相关的方法::
interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了
isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
Thread类的静态方法interrupted(),返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
6.2 线程等待唤醒机制
这一节是线程通信的续接,会更深入
LockSupport
是什么
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport
中的 park()
和 unpark()
的作用分别是阻塞线程和解除阻塞线程
LockSupport
类使用了一种名为 Permit
(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),
permit
只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
LockSupport
的主要方法:
park() /park(Object blocker)
:阻塞当前线程/阻塞传入的具体线程
permit 默认是零,所以一开始调用 park()
方法,当前线程就会阻塞,直到别的线程将当前线程的 permit
设置为1时,park
方法会被唤醒,然后会将 permit 再次设置为零并返回。
unpark(Thread thread)
:唤醒处于阻塞状态的指定线程
调用 unpark(thread)
方法后,就会将 thread
线程的许可 permit
设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒 thread 线程,即之前阻塞中的 LockSupport.park()
方法会立即解除阻塞。
三种让线程等待唤醒的方法:
- 使用
Object
中的wait()
方法让线程等待,使用Object
中的notify()
方法唤醒线程 - 使用 JUC 包中
Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程 LockSupport
类可以阻塞当前线程以及唤醒指定被阻塞的线程
方式一:
正常情况:
public static void main(String[] args) {
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();
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t" + "---发出通知");
}
}, "t2").start();
}
// 结果
t1 ---come in
t2 ---发出通知
t1 ---被唤醒
异常情况1,取消使用 synchronized
关键字。即 wait
方法和 notify
方法,两个都去掉同步代码块:
// 结果
Exception in thread "t2" java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.notify(Native Method)
at com.lt.juc.interrupt.LockSupportDemo.lambda$main$1(LockSupportDemo.java:36)
at java.base/java.lang.Thread.run(Thread.java:833)
Exception in thread "t1" java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.wait(Native Method)
at java.base/java.lang.Object.wait(Object.java:338)
at com.lt.juc.interrupt.LockSupportDemo.lambda$main$0(LockSupportDemo.java:26)
at java.base/java.lang.Thread.run(Thread.java:833)
异常情况2,将 notify
放在 wait
方法前面:
public static void main(String[] args) {
new Thread(() -> {
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(3);
} 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();
}
// 结果 可以发现 程序还在运行,被阻塞,无法唤醒
t2 ---发出通知
t1 ---come in
总结
wait
和notify
方法必须要在同步块或者方法里面,且成对出现使用- 先
wait
后notify
才OK
方式二:
正常情况:
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
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();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t" + "---发出通知");
} finally {
lock.unlock();
}
}, "t2").start();
}
// 结果
t1 ---come in
t2 ---发出通知
t1 ---被唤醒
异常情况1,去掉 lock/unlock
:
// 结果
Exception in thread "t2" java.lang.IllegalMonitorStateException at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1473) at com.lt.juc.interrupt.LockSupportDemo.lambda$main$1(LockSupportDemo.java:42)
at java.base/java.lang.Thread.run(Thread.java:833)
Exception in thread "t1" java.lang.IllegalMonitorStateException at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.enableWait(AbstractQueuedSynchronizer.java:1516) at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1611) at com.lt.juc.interrupt.LockSupportDemo.lambda$main$0(LockSupportDemo.java:30)
at java.base/java.lang.Thread.run(Thread.java:833)
异常情况2,先 signal
后 await
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
// 暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(3);
} 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();
}
// 结果 可以发现 程序还在运行,被阻塞,无法唤醒
t2 ---发出通知
t1 ---come in
结论:
Condtion
中的线程等待和唤醒方法之前,需要先获取锁- 一定要先
await
后signal
,不要反了
由上述两种方法可知,Object
和 Condition
使用的限制条件:
- 线程先要获得并持有锁,必须在锁块(
synchronized
或lock
)中 - 必须要先等待后唤醒,线程才能够被唤醒
方法三:
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");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + "---被唤醒");
}, "t1");
t1.start();
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t" + "---发出通知");
}, "t2").start();
}
// 结果 可以发现使用 LockSupport 方法 无锁块要求 并且 唤醒等待顺序也无要求 但是切记,permit 的值最多为1
t2 ---发出通知
t1 ---come in
t1 ---被唤醒
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "---come in");
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + "---被唤醒");
}, "t1");
t1.start();
new Thread(() -> {
LockSupport.unpark(t1);
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t" + "---发出通知");
}, "t2").start();
}
// 结果 我们使用了 2次 park 和2次 unpark 但是程序还是被阻塞 就是因为 许可证 permit 最大为1
t1 ---come in
t2 ---发出通知
更多文章在我的语雀平台:https://www.yuque.com/ambition-bcpii/muziteng