1、线程中断
1.1 线程中断的定义
一个线程不应该由其他线程来强制中断或者停止,而是应该由线程自己自行停止。所以,很多的方法已经被废弃了,如Thread.stop、Thread。suspend、Thread.resume。
在Java中没有办法立即停止一个线程,然而停止线程很重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制----中断,也即中断标识协商机制。
中断是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全由程序员自己实现。 若要中断一个线程,需要手动调用该线程的interrupt方法,该方法仅仅是将线程对象的中断标识设为true,接着手动写代码不断检测当前线程的标识位。
1.2 线程中断的三个API
| API签名 | 说明 |
|---|---|
| public void interrupt() | 实例方法:仅仅是设置线程的中断状态为true,发起协商而不是立即中断 |
| public static boolean interrupted() | 判断线程是否被中断并清除当前中断状态,一共做了两件事:返回线程的中断状态,判断是否已被中断;将当前线程的中断状态清零并重新设置为false,清除线程的中断状态 |
| public boolean isInterrupted() | 判断当前线程是否被中断(检测中断标志位的值) |
1.3 面试题中中断机制的考点
1.3.1 如何停止中断运行中的线程
① volatile关键字
通过使用volatile关键字,让两个线程共用一个标志位isStop,第一个线程使用这个标志位用于检测判断是否需要中断,第二个线程通过对isStop标志位进行设置来协商让第一个线程中断。
以下方法的检测是将t1线程通过if(isStop)来判断是否中断,t2线程通过isStop=true对其进行协商中断
static volatile boolean isStop = false;
private static void UsingVolatileToInterruptedThread() {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println("t1 \t isStop标志位被修改为true,线程中断");
break;
}
System.out.println("t1 ----- Using volatile to Interrupted");
}
},"t1").start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
isStop = true;
},"t2").start();
}
② AtomicBoolean原子类
通过使用AtomicBoolean原子类,让两个线程共用一个AtomicBoolean,第一个线程使用这个标志位用于检测判断是否需要中断,第二个线程通过对AtomicBoolean标志位进行设置,即调用atomicBoolean.set(true)方法让第一个线程中断。
以下方法的检测是将t1线程通过atomicBoolean.get()来判断是否中断,t2线程通过atomicBoolean.set(true);对其进行协商中断。
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
private static void UsingAtomicBooleanToInterruptedThread() {
new Thread(() -> {
while (true) {
if (atomicBoolean.get()) {
System.out.println("t1 \t atomicBoolean被修改为true,线程中断");
break;
}
System.out.println("t1 ----- Using AtomicBoolean to Interrupted");
}
},"t1").start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
atomicBoolean.set(true);
},"t2").start();
}
③ Thread内部的Api
在前面的1.2 线程中断的三个API中介绍到的三个Api来使线程进行中断。
Ⅰ、代码案例测试
通过使用Thread内置的Api,第一个线程通过使用Thread.currentThread().isInterrupted()用于检测判断是否需要中断。第二个线程通过对Thread.currentThread().isInterrupted()标志位进行设置,即调用interrupt()方法让第一个线程中断;也可以在运行的过程中第一个线程觉得自己需要中断了,因此自己调用了interrupt()方法中断。
以下方法的检测是将t1线程通过Thread.currentThread().isInterrupted()来判断是否需要中断
2线程通过t1.interrupt()方对其进行协商中断,也可以是t1线程自身调用t1.interrupt()来进行中断。
private static void UsingThreadApiToInterruptedThread() {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("t1 \t isInterrupted()被修改为true,线程中断");
break;
}
System.out.println("t1--Using Thread Interrupted Api to Interrupted");
}
}, "t1");
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 通过其他线程来使得t1线程中断
/*new Thread(() -> {
t1.interrupt();
},"t2").start();*/
// t1线程自己中断
t1.interrupt();
}
Ⅱ、interrupt()方法的源码分析
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
Ⅲ、isInterrupted()方法的源码分析
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
1.3.2 当前线程的中断标识位true,是不是线程就立刻停止
当对一个线程调用interrupt()时
- 如果线程处于
正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。 - 如果线程处于被
阻塞状态,在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
① 正常活动状态的线程
现有如下的代码,t1线程循环执行,经过2ms后,t2线程与t1线程协商
private static void ActiveThreadIsInterrupted() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("------" + i);
}
System.out.println("2将t1线程的中断标志位为:" + Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
System.out.println("t1线程的中断标志位默认为:" + t1.isInterrupted());
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
t1.interrupt();
System.out.println("将t1线程的中断标志位设置为:" + t1.isInterrupted());
},"t2").start();
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1将t1线程的中断标志位为:" + t1.isInterrupted());
}
此时运行的结果为:
t1线程的中断标志位默认为:false
------0
------1
------2
------3
...
------279
------280
将t1线程的中断标志位设置为:true
------281
------282
...
------878
------879
1将t1线程的中断标志位为:true
------880
------881
...
------999
2将t1线程的中断标志位为:true
② 被阻塞状态的线程
private static void SleepThreadIsInterrupted() {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("t1 \t 中断标志位:" + Thread.currentThread().isInterrupted() + "程序终止");
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----- hello ");
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> t1.interrupt(), "t2").start();
}
此时运行后的结果为:
----- hello
----- hello
----- hello
----- hello
java.lang.InterruptedException: sleep interrupted
----- hello
----- hello
----- hello
----- hello
...
发现,报了异常之后,程序仍然在继续执行。 接着,我们在线程进入睡眠状态后的catch代码块中再次添加打断信号。
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
此时运行的结果为:
----- hello
----- hello
----- hello
----- hello
java.lang.InterruptedException: sleep interrupted
----- hello
t1 中断标志位:true 程序终止
Process finished with exit code 0
可以发现,虽然仍然是抛出了异常,但是程序正常执行完毕了,而不像刚刚,抛了异常,但是程序却仍在运行。
这是为什么呢?
在jdk中有关interrupt()方法的介绍如下:
中断此线程。
除非当前线程正在中断(始终允许),否则将调用此线程的
checkAccess方法,这可能会导致抛出SecurityException如果该线程阻塞的调用
wait()、wait(long)或wait(long, int)的方法Object类,或的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int),这个类的方法,那么它的中断状态将被清除,并且将收到InterruptedException。如果在
InterruptibleChannel上的I/O操作中阻止该线程,则通道将关闭,线程的中断状态将被设置,线程将收到ClosedByInterruptException。如果该线程在
Selector中被阻塞,则线程的中断状态将被设置,它将立即从选择操作返回,可能具有非零值,就像调用选择器的wakeup方法一样。如果以前的条件都不成立,则将设置该线程的中断状态。
中断不活动的线程不会产生任何影响。
简单而言就是,当线程因调用wait()、join()、sleep()及对应的重载方法时,如果此时自己或其他线程调用了interrupt()方法让该线程进入阻塞状态,那么此时该次interrupt()无效(即此时的标志位的值仍然为false),且会抛出一个InterruptedException异常,同时,会让该线程从阻塞状态恢复到运行状态。
所以,在第一个代码演示中,线程t2中调用了t1.interrupt()时,会抛出异常,但是程序并没有终止。而在第二个代码演示中,当抛出异常之后,我们在catch(){}代码块中又添加了一次interrupt()方法,虽然这一次是自身调用的,但是效果也是一样。因为t2线程调用时已经将t1线程唤醒,因此此时再次调用时,便可以正常的将标志位的值置为true,而导致程序的终止。
1.3.3 谈谈你对静态方法Thread.interrupted()的理解
当有线程调用静态方法interrupted()时,会返回当前线程的中断状态位,并且将线程的中断状态位设置为false。其底层与isInterrupted()方法一样,都是调用了isInterrupted(boolean ClearInterrupted)方法,而区别在于interrupted()方法传入的是true,而isInterrupted()传入的是false。
代码测试案例:
private static void testStaticInterruptedMethod() {
System.out.println("t1 \t" + Thread.interrupted());
System.out.println("t1 \t" + Thread.interrupted());
System.out.println("------1");
Thread.currentThread().interrupt();
System.out.println("------2");
System.out.println("t1 \t" + Thread.interrupted());
System.out.println("t1 \t" + Thread.interrupted());
}
输出的结果为:
main false
main false
------1
------2
main true
main false
可以看到,一开始没有对main线程进行中断时,两次调用的结果都是false,main线程自己中断之后,再次调用时,第一次返回的是true,但第二次返回的是false。
2、线程等待唤醒机制
2.1 3种让线程等待和唤醒的方法
2.1.1 Object类中的wait和notify方法
① 代码测试
Ⅰ、正常运行
private static void waitAndNotifyToThread() {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println("---------- t1 come in");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------- t1 被唤醒");
}
},"t1").start();
// 线程暂停1秒钟,确保t1线程先启动
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (o) {
o.notify();
System.out.println("---------- t2发出通知唤醒想持有o对象锁的阻塞线程");
}
},"t2").start();
}
此时的运行结果为:
---------- t1 come in
---------- t2发出通知唤醒想持有o对象锁的阻塞线程
---------- t1 被唤醒
程序正常运行,也正常终止了。
Ⅱ、去掉同步代码块
private static void waitAndNotifyToThreadWithoutSynchronized() {
Object o = new Object();
new Thread(() -> {
System.out.println("---------- t1 come in");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------- t1 被唤醒");
},"t1").start();
// 线程暂停1秒钟,确保t1线程先启动
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
o.notify();
System.out.println("t2发出通知唤醒想持有o对象锁的阻塞线程");
},"t2").start();
}
此时的运行结果为:
---------- t1 come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
Exception in thread "t2" java.lang.IllegalMonitorStateException
会报异常信息,抛出IllegalMonitorStateException异常。
Ⅲ、先notify再wait
private static void notifyBeforeWaitToThread() {
Object o = new Object();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o) {
System.out.println("---------- t1 come in");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------- t1 被唤醒");
}
},"t1").start();
new Thread(() -> {
synchronized (o) {
o.notify();
System.out.println("---------- t2发出通知唤醒想持有o对象锁的阻塞线程");
}
},"t2").start();
}
此时的运行结果为:
---------- t2发出通知唤醒想持有o对象锁的阻塞线程
---------- t1 come in
但是还有一条语句没有打印:---------- t1 被唤醒
同时,程序并没有终止。
② 小结
wait和notify都需要在synchronized代码块中使用,否则会报IllegalMonitorStateException异常。wait必须在notify之前使用,否则wait线程不能正常被唤醒
2.1.2 Condition接口中的await和signal方法
① 代码测试
Ⅰ、正常运行
private static void awaitAndSignalToThread() {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("-------- t1 come in");
condition.await();
System.out.println("-------- t1被唤醒");
} 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("-------- t2发出唤醒通知");
} finally {
lock.unlock();
}
},"t2").start();
}
此时的运行结果为:
-------- t1 come in
-------- t2发出唤醒通知
-------- t1被唤醒
可以看到,在这种情况下是正常运行的。
Ⅱ、没有lock锁
private static void awaitAndSignalWithoutLock() {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try {
System.out.println("-------- t1 come in");
condition.await();
System.out.println("-------- t1被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
try {
condition.signal();
System.out.println("-------- t2发出唤醒通知");
} finally {
}
},"t2").start();
}
此时的运行结果为:
-------- t1 come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
Exception in thread "t2" java.lang.IllegalMonitorStateException
可以看到,此时仍然同Object类中的wait和notify方法一样会报IllegalMonitorStateException异常信息。
Ⅲ、先signal再await
private static void signalBeforeAwaitToThread() {
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("-------- t1 come in");
condition.await();
System.out.println("-------- t1被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println("-------- t2发出唤醒通知");
} finally {
lock.unlock();
}
},"t2").start();
}
此时的运行结果为:
-------- t2发出唤醒通知
-------- t1 come in
程序不能正常执行
② 小结
await和signal都需要在lock锁的基础上去使用signal必须跟在await的后面,否则,不能正常唤醒线程
2.1.3 LockSupport中的park等待和unpark唤醒
① LockSupport是什么
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),但与Semaphore不同的是,许可的累加上限是1。
此类与使用它的每个线程关联一个许可证(在Semaphore类的意义上)。如果许可证可用,将立即返回park,并在此过程中消耗;否则可能会阻止。如果许可证尚不可用,则致电unpark获得许可证。(与Semaphore不同,许可证不会累积。最多有一个。)
② LockSupport的主要方法
Ⅰ、阻塞
park() / park(Object locker),阻塞当前线程或者是阻塞传入的具体线程。
/**
* 出于线程调度目的禁用当前线程,除非允许可用。
* 如果许可证可用,则将其使用,呼叫立即返回;否则,当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一:
* 其他一些线程以当前线程作为目标进行调用 unpark ;或者
* 其他线程 中断 当前线程;或
* 虚假调用(即无缘无故)返回。
* 此方法 不会 报告哪些导致该方法返回。调用方应重新检查导致线程首先停放的条件。例如,调用方还可以确定线程在返回时的中断状态。
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void park() {
UNSAFE.park(false, 0L);
}
Ⅱ、唤醒
unpark(Thread t) ,唤醒处于阻塞状态的指定线程。
// 为给定线程提供许可证(如果尚不可用)。如果线程被阻塞,那么它将取消阻塞 park 。否则,保证其下一次调用 park 不会阻塞。如果给定线程尚未启动,则不保证此操作有任何效果。
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
③ 代码测试
Ⅰ、正常运行
private static void lockSupportParkAndUnparkToThread() {
Thread t1 = new Thread(() -> {
System.out.println("--------------- t1线程 come in");
LockSupport.park();
System.out.println("--------------- t1线程 被唤醒");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println("--------------- t2线程发出唤醒通知");
},"t2").start();
}
此时运行的结果为:
--------------- t1线程 come in
--------------- t2线程发出唤醒通知
--------------- t1线程 被唤醒
可以发现,park()和unpark()方法是不需要在锁块中使用的,可以直接使用,而且代码看上去会更简洁。
Ⅱ、先unpark再park
private static void lockSupportUnparkBeforePark() {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--------------- t1线程 come in " + System.currentTimeMillis());
LockSupport.park();
System.out.println("--------------- t1线程 被唤醒 " + System.currentTimeMillis());
}, "t1");
t1.start();
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println("--------------- t2线程发出唤醒通知");
},"t2").start();
}
此时的运行结果为:
--------------- t2线程发出唤醒通知
--------------- t1线程 come in 1684032061621
--------------- t1线程 被唤醒 1684032061621
后面的数字是执行该代码时系统当前的时间
此时可以发现,程序正常运行了,而且t1线程的两条代码的执行时间一致,说明t2线程先unpark()之后,t1线程的park()已经不生效了。
上述的两点都可以发现,LockSupport比前面的Object和Condition都要好。
Ⅲ、多个unpark和park
同样是先执行unpark再执行park
private static void parksAndUnparks() {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--------------- t1线程 come in " + System.currentTimeMillis());
LockSupport.park();
LockSupport.park();
LockSupport.park();
System.out.println("--------------- t1线程 被唤醒 " + System.currentTimeMillis());
}, "t1");
t1.start();
new Thread(() -> {
LockSupport.unpark(t1);
LockSupport.unpark(t1);
LockSupport.unpark(t1);
System.out.println("--------------- t2线程发出唤醒通知");
},"t2").start();
}
此时运行的结果为:
--------------- t2线程发出唤醒通知
--------------- t1线程 come in 1684033025761
可以发现,此时的t1线程不能正常执行,不会被正常唤醒。
这是因为,unpark()在发通行证时,只会发一个,不会累加,因此同时多次调用unpark()和只调用一次其实都是一样的,都只有一个通行证。因此在t1线程中去调用park()的时候,第一个park()将通行证消耗了之后,其他的park()就需要重新等待通行证,因此程序就不能正常执行了。