5 LockSupport与线程中断
目录
线程中断机制
阿里蚂蚁金服面试题
void | interrupt() | 中断此线程 |
---|
static boolean | interrupted() | 测试当前线程是否已被中断 | boolean | isInterrupted() | 测试此线程是否已经被中断 |
-
三个方法了解过吗?用在哪? -
如何停止一个运行中的线程? -
如何中断一个运行中的线程??
什么是中断机制?
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制——中断。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
- eg.顾客在无烟餐厅中吸烟,服务员希望他别吸烟了,不是强行停止他吸烟,而是给他的标志位打为true,具体的停止吸烟还是要顾客自己停止。(体现了协商机制)
中断的相关API方法之三大方法说明
public void interrupt() | 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程 |
---|
public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。) | public boolean isInterrupted() | 实例方法,判断当前线程是否被中断(通过检查中断标志位) |
大厂面试题:如何使用中断标识停止线程?
1如何停止中断运行中的线程?
① 通过一个volatile变量实现
- volatile保证了可见性,t2修改了标志位后能马上被t1看到
public class interruptDemo {
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();
}
}
② 通过AtomicBoolean(原子布尔型)
public class interruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
m1_volatile();
}
public static void m1_volatile() {
new Thread(()->{
while(true){
if(atomicBoolean.get()){
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(()->{
atomicBoolean.set(true);
},"t2").start();
}
}
③ 通过Thread类自带的中断api方法实现
public static void main(String[] args) {
m1_volatile();
}
public static void m1_volatile() {
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 ");
}
}, "t1");
t1.start();
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
t1.interrupt();
},"t2").start();
}
}
—API源码分析
实例方法interrupt(),没有返回值
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);
实例方法isInterrupted,返回布尔值
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
说明
2 当前线程的中断标识为true,是不是线程就立刻停止?
public class InterruptDemo02 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i = 0;i < 300;i ++){
System.out.println("---------" + i);
}
System.out.println("after t1.interrupt()---第2次----"+Thread.currentThread().isInterrupted());
},"t1");
t1.start();
System.out.println("before t1.interrupt()----"+t1.isInterrupted());
t1.interrupt();
try {TimeUnit.MILLISECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("after t1.interrupt()---第1次---"+t1.isInterrupted());
try {TimeUnit.MILLISECONDS.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("after t1.interrupt()---第3次---"+t1.isInterrupted());
}
}
后手案例-深入
public class InterruptDemo03 {
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) {
e.printStackTrace();
}
System.out.println("-----hello InterruptDemo03");
}
},"t1");
t1.start();
try {TimeUnit.MILLISECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(() -> t1.interrupt()).start();
}
}
-
前文
- ② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的
interrupt 方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。
sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在catch没有通过th.interrupt()方法再次将中断标志设置为true,这就导致无限循环了
小总结
- 中断只是一种协同机制,修改中断标识位仅此而已,而不是立刻stop打断
3 静态方法Thread.interrupted(),谈谈你的理解
-
api里的第二个
- public static boolean interrupted()
- 静态方法,
Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:1 返回当前线程的中断状态2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。) |
public class InterruptDemo04 {
public static void main(String[] args) {
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();
System.out.println("-----2");
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
}
}
-
看下源码,interrupted() 对比isInterrupted() public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
LockSupport是什么
-
官方解释:用于创建锁和其他同步类的基本线程阻塞原语。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyfrOAqO-1654754693266)(image/image_SOr3KJQSy_.png)] -
核心就是park() 和unpark() 方法
-
park() 方法是阻塞线程 -
unpark() 方法是解除阻塞线程
线程等待唤醒机制
3种让线程等待和唤醒的方法
-
使用Object中的wait() 方法让线程等待,使用Object中的notify() 方法唤醒线程 -
使用JUC包中Condition 的await() 方法让线程等待,使用signal() 方法唤醒线程 -
LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程
①Object类中的wait和notify方法实现线程等待和唤醒
-
正常 public class LockSupportDemo
{
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(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
}
},"t2").start();
}
}
-
异常1—去掉synchronized
- 说明要使用
wait 和notify 必须加synchronized public class LockSupportDemo
{
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(3L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
},"t2").start();
}
}
-
异常2—把notify和wait的执行顺序对换
public class LockSupportDemo
{
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();
}
}
小总结
②Condition接口中的await后signal方法实现线程的等待和唤醒
-
正常 public class LockSupportDemo
{
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();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+"\t"+"我要进行唤醒");
},"t2").start();
}
}
-
异常1原理同上
- 仍然返回
IllegalMonitorStateException -
异常2 原理同上
小总结
Object和Condition使用的限制条件
③LockSupport类中的park等待和unpark唤醒
是什么
-
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作 -
官网解释 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-za4dJOuv-1654754693269)(image/image_E5vM4GxxSB.png)]
-
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。 -
LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit), -
permit(许可)只有两个值1和0,默认是0。0 是阻塞,1是唤醒 -
可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
许可(Permit)
主要方法
API
阻塞
-
park()/park(Object blocker) -
调用LockSupport.park() 时,发现它调用了unsafe类 ,并且默认传了一个0 public static void park() {
UNSAFE.park(false, 0L);
}
- permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,
然后会将permit再次设置为零并返回。
唤醒
- 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
代码
-
正常+无锁块要求 public class LockSupportDemo
{
public static void main(String[] args) {
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();
new Thread(()->{
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
},"t2").start();
}
}
-
之前错误的先唤醒后等待,LockSupport照样支持 public class LockSupportDemo
{
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis());
},"t1");
t1.start();
new Thread(()->{
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
},"t2").start();
}
}
sleep方法3秒后醒来,执行park无效,没有阻塞效果,解释如下。先执行了unpark(t1)导致上面的park方法形同虚设无效,时间是一样的 - 类似于高速公路的ETC,提前买好了通行证unpark,到闸机处直接抬起栏杆放行了,没有park拦截了。
-
成双成对要牢记
最后
-
许可证是只要一个的 public class LockSupportDemo
{
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis());
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis());
},"t1");
t1.start();
new Thread(()->{
LockSupport.unpark(t1);
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
},"t2").start();
}
}
-
小总结 Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。 Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结 底, Lock Support调用的Unsafe中的native代码。 Lock Support提供park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程 Lock Support和每个使用它的线程都有一个许可(permit) 关联。 每个线程都有一个相关的permit, permit最多只有一个, 重复调用un park也不会积累凭证。 形象的理解 线程阻塞需要消耗凭证(permit) , 这个凭证最多只有1个。 当调用方法时 *如果有凭证,则会直接消耗掉这个凭证然后正常退出; *如果无凭证,就必须阻塞等待凭证可用; 而****则相反, 它会增加一个凭证, 但凭证最多只能有1个, 累加无效。
面试题
-
为什么可以突破wait/notify的原有调用顺序? 因为un park获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。 先发放了凭证后续可以畅通无阻。 -
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程? 因为凭证的数量最多为1, 连续调用两次un park和调用一次un park效果一样, 只会增加一个凭证; 而调用两次park却需要消费两个凭证, 证不够, 不能放行。
|