【线程、锁】LockSupport 阻塞和中断

本文探讨了LockSupport的park方法如何响应中断,以及中断如何影响后续的阻塞行为。通过示例代码展示了park方法的特点,包括如何通过清除中断标记使线程再次阻塞,以及在AQS中的实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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()”清除中断标记了。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值