文章目录
1.目的
我们知道LockSupport 可以调用park()方法阻塞线程,也可以调用unpark(Thread t)主动唤醒线程。
另外,我们知道对于常规的阻塞方法,譬如sleep和wait,是可以响应中断的,那么LockSupport 阻塞时对中断是什么响应效果?
结论: 注意重载方法
- 调用park()后,一旦响应中断,那么后续再次park()时,就失效了
2. 验证
2.1 park()会让线程阻塞
我们创建了一个线程t0,该线程会一直在循环的执行park()阻塞操作,我们在主线程会通过unpark( t0)唤醒一次t0,然后t0又会陷入阻塞,这次就永远阻塞,因为没有人再去唤醒它:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j
public class Thread_Interrupt {
public static void main(String[] args) {
Thread t0 = new Thread(new Runnable() {
@Override
public void run() {
Thread current = Thread.currentThread();
log.info("{},开始执行!", current.getName());
for (; ; ) {//spin 自旋
log.info("准备park住当前线程:{}....", current.getName());
LockSupport.park();
log.info("当前线程{}已经被唤醒....", current.getName());
//[0]打印线程中断标识,注意:Thread.interrupted()和Thread.currentThread().isInterrupted的区别
//前者会清除标记,后者不会
//log.info("当前线程{}是否被中断....{}", current.getName(), Thread.interrupted());
}
}
}, "t0");
t0.start();
try {
Thread.sleep(2000);
log.info("准备唤醒{}线程!", t0.getName());
LockSupport.unpark(t0);
Thread.sleep(2000);
//[1]测试中断
// t0.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行结果:
19:30:50.546 [t0] INFO com.test.Thread_Interrupt - t0,开始执行!
19:30:50.558 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
19:30:52.547 [main] INFO com.test.Thread_Interrupt - 准备唤醒t0线程!
19:30:52.547 [t0] INFO com.test.Thread_Interrupt - 当前线程t0已经被唤醒....
19:30:52.547 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
分析:t0会唤醒一次后,就永远陷入阻塞中。
2.2 park()会让线程阻塞,通过中断可以唤醒,后续的阻塞会失效
我们基于2.1的例子,在主线程额外调用一次中断,可以唤醒一次阻塞,但是唤醒之后,即使再次调用part()试图去阻塞,比如循环体中再次调用park(),阻塞效果会失效:
放开[1]处中断代码:
t0.interrupt();
执行结果,发现在不断的打印循环体中的日志
,说明后续的阻塞失效
了:
....打印太快了,省略前面的
7:51.693 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 当前线程t0已经被唤醒....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 当前线程t0已经被唤醒....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 当前线程t0已经被唤醒....
19:57:51.693 [t0] INFO com.test.Thread_Interrupt - 准备park住当前线程:t0....
....后面很多,也省略
注意:调用park()
失效的原因是,该方法内会判断当前线程的中断标识
是否为true
,为true的话,会失效;因此当你清除该标记,例如调用Thread.interrupted()
(静态方法,该方法调用后会将中断标示位清除,即重新设置为false)后,就会又能进入阻塞!!
2.2.1 中断唤醒后续的阻塞会失效的原理
上面蓝色字体已经解释了原因,我们来验证下:
在2.2章节的基础上,再放开[0]处代码,此时,打印中的代码会清除标记:
log.info("当前线程{}是否被中断....{}", current.getName(), Thread.interrupted());
执行结果,发现在没有不断的打印循环体中的日志
,说明后续的阻塞生效
了:
15:08:59.358 [t0] INFO com.test.spring.Thread_interrupt2 - t0,开始执行!
15:08:59.369 [t0] INFO com.test.spring.Thread_interrupt2 - 准备park住当前线程:t0....
15:09:01.352 [main] INFO com.test.spring.Thread_interrupt2 - 准备唤醒t0线程!
15:09:01.352 [t0] INFO com.test.spring.Thread_interrupt2 - 当前线程t0已经被唤醒....
15:09:01.352 [t0] INFO com.test.spring.Thread_interrupt2 - 当前线程t0是否被中断....false
15:09:01.352 [t0] INFO com.test.spring.Thread_interrupt2 - 准备park住当前线程:t0....
15:09:03.352 [t0] INFO com.test.spring.Thread_interrupt2 - 当前线程t0已经被唤醒....
15:09:03.352 [t0] INFO com.test.spring.Thread_interrupt2 - 当前线程t0是否被中断....true
15:09:03.353 [t0] INFO com.test.spring.Thread_interrupt2 - 准备park住当前线程:t0....
分析:当标记被清除后,那么后续的中断又生效了!因此要注意
2.2.2 park()中断唤醒的经典用例
AQS (AbstractQueuedSynchronizer)底层就是调用park()方法,保证阻塞被唤醒后,如果加锁失败,可以再次阻塞,减轻资源消耗,在出现等待队列时,队列中的某个节点是可以响应中断的:
红色框内代码调用park(),并监听中断,并且AQS在线程响应中断后,会清除标记,如绿色框内的代码
我们来验证下AQS中的中断:
我们创建10线程,访问同一个ReentrantLock锁(底层调用AQS),持有锁的那个线程实际上永远不会释放那个锁,其余9个线程会在阻塞队列,我们对第四个线程执行中断,唤醒一次,通过断点发现,第四个线程会响应中断,然后继续跟踪,发现会再次进入阻塞状态:
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class Thread_Interrupt {
/**
* 可重入锁,怎么实现类似于synchronized的功能
*/
public static ReentrantLock lock = new ReentrantLock(true);
static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
//存放10个线程
List<Thread> list = new ArrayList<>();
//创建10个线程,只有一个持有锁,并一直持有,那么后续的其他线程都在阻塞队列中
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
//等待flag结束信号,实际上永远不会退出while,故意第一个线程持有锁,其他线程一直在等待队列中
while (true) {
if (flag) {
break;
}
}
lock.unlock();
}, "t-" + i);
list.add(t);
t.start();
//增加时间间隔,保证第一个线程能持有锁
TimeUnit.MILLISECONDS.sleep(10);
}
try {
//等待2s,保证其他线程在阻塞队列
Thread.sleep(2000);
//[5]取第四个线程,将其中断
list.get(3).interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
idea执行步骤:
步骤1:在代码[5]处设置断点,注意断点类型为thread类型
至于断点设置为thread类型de 原因参见 《IntelliJ IDEA - Debug 调试多线程程序》
此时第一个线程处于running状态,其他的都是wait:
步骤2
在AQS代码处设置断点,在interrupted = true;
处设置断点
执行主线程的[5]处断点,目的是触发第四个线程的中断操作
步骤3 切换断点视图到第四个线程
debug执行结果,蓝色背景表示断点进来了,说明中断响应了:
如绿色箭头所示,t-3 即第四个线程被唤醒了,此时状态是running,其他的线程仍是wait。
然后一步步跟踪下去,发现t-3线程进入循环体后,再次进入阻塞!能再次陷入阻塞原因就是调用“Thread.interrupted()”清除中断标记了。