请教大家一个问题,线程池何时清理中断状态的,追源码没找到 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
git00ll
V2EX    Java

请教大家一个问题,线程池何时清理中断状态的,追源码没找到

  •  
  •   git00ll 2022 年 3 月 9 日 2486 次点击
    这是一个创建于 1509 天前的主题,其中的信息可能已经有所发展或是发生改变。
     static class MyRun implements Runnable { @Override public void run() { System.out.println("MyRun 当前线程池:" + Thread.currentThread().hashCode()); Thread.currentThread().interrupt(); System.out.println("MyRun 当前线程池中断状态:" + Thread.currentThread().isInterrupted()); } } static class MyRun2 implements Runnable { @Override public void run() { System.out.println("MyRun2 当前线程池:" + Thread.currentThread().hashCode()); System.out.println("MyRun2 当前线程池中断状态:" + Thread.currentThread().isInterrupted()); } } public static void main(String[] args) { ExecutorService ex = Executors.newSingleThreadExecutor(); ex.execute(new MyRun()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(); } ex.execute(new MyRun2()); } 

    如上述代码,输出结果如下,为何 MyRun2 中读取的中断状态是 false 呢

    MyRun 当前线程池:352001653 MyRun 当前线程池中断状态:true MyRun2 当前线程池:352001653 MyRun2 当前线程池中断状态:false 
    19 条回复    2022-03-11 10:28:04 +08:00
    qfdk
        1
    qfdk  
    PRO
       2022 年 3 月 9 日 via iPhone
    不是有个调试工具么 jconsole
    cheng6563
        2
    cheng6563  
       2022 年 3 月 9 日   1
    MyRun2 不是本来就没 interrupt 吗?
    uSy62nMkdH
        3
    uSy62nMkdH  
       2022 年 3 月 9 日
    @cheng6563 正解
    git00ll
        4
    git00ll  
    OP
       2022 年 3 月 9 日
    @cheng6563
    @uSy62nMkdH
    因为是个单线程的线程池,Myrun 和 Myrun2 其实用的是同一个线程。通过输出的 thread hashcode()也能看出线程是同一个。

    Myrun 里面已经将线程设为中断状态了,理论上 Myrun2 运行的时候应该是处于中断状态的,但是却没有。所以一定有一个地方将该线程的状态标识移除了,我没找到在哪里实现的。
    git00ll
        5
    git00ll  
    OP
       2022 年 3 月 9 日
    @qfdk 这个好像不能看出线程的中断状态
    litchinn
        6
    litchinn  
       2022 年 3 月 9 日
    `ThreadPoolExecutor` 中 `execute`有如下一段注释
    litchinn
        7
    litchinn  
       2022 年 3 月 9 日   1
    @litchinn
    ```
    * 2. If a task can be successfully queued, then we still need
    * to double-check whether we should have added a thread
    * (because existing ones died since last checking) or that
    * the pool shut down since entry into this method. So we
    * recheck state and if necessary roll back the enqueuing if
    * stopped, or start a new thread if there are none.
    ```
    在 addWorker 中的 new Worker 时有重置状态操作
    litchinn
        8
    litchinn  
       2022 年 3 月 9 日
    ```
    /**
    * Creates with given first task and thread from ThreadFactory. * @param firstTask the first task (null if none)
    */Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
    }
    ```
    nothingistrue
        9
    nothingistrue  
       2022 年 3 月 10 日
    isInterrupted 表示的不是线程是否中断(阻塞),而是该线程是否刚收到其他线程给他的中断信号。你这里只有一个线程,没有线程之间的通信,所以 isInterrupted 应该恒定返回 false 。不过,你的 Run1 自己给自己了一个中断信号,导致 isInterrupted = true 。

    interrupt()这个方法(注意还有一个 interrupted 方法,这俩不一样)的实际作用是给目标线程一个“请你中断”的信号,并不是中断目标线程,目标线程做啥反应,是由目标线程自身决定的。Thread.currentThread().interrupt(),这样自己给自己中断信号,是一个很怪的操作。
    nothingistrue
        10
    nothingistrue  
       2022 年 3 月 10 日
    Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。
    git00ll
        11
    git00ll  
    OP
       2022 年 3 月 10 日
    @litchinn
    @nothingistrue

    自答一下:

    首先我们要知道,如果我顺序的执行以下代码,是会抛出中断异常的
    ```
    BlockingQueue<Runnable> queue = getQueue();
    Thread.currentThread().interrupt();
    queue.take(); //这里抛出中断异常
    ```
    虽然是先设置的中断状态,后执行的堵塞队列的 take()方法,但 take()方法内部会检测当前线程的中断状态。



    那单线程池是如何工作的呢?可以认为一个堵塞队列,加一个死循环的线程。线程从队列中获取任务执行,而这个获取的过程就是调用了
    BlockQueue 的 take 方法。

    所以这就解释了题目的问题,
    1. 我们在 MyRun 中将当前线程设为中断状态,MyRun 执行完成后线程处于中断状态。
    2. 然后开始从队列中 take 下一个任务,此时就会抛出中断异常,并清除中断状态
    3. 线程池抓住中断异常,忽略并继续 take 下一个任务
    4. 此时 take 到 MyRun2 任务,并执行它。这样 MyRun2 再检测中断状态就是 false 了


    总结就是,线程池在 take 下一个任务时,如果抛出中断异常,会抓住异常并继续 take 下一个任务,而抛出中断异常时中断状态就被清除了。


    这一快的源码如下, 正式在 while 循环的条件里,一直调用 getTask() 方法,
    ```
    final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    while (task != null || (task = getTask()) != null) {
    w.lock();
    // If pool is stopping, ensure thread is interrupted;

    ```

    getTask 方法,正是调用了堵塞队列的 take 方法,并忽略了中断异常
    ```
    private Runnable getTask() {
    for (;;) {
    try {
    Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();
    if (r != null)
    return r;
    timedOut = true;
    } catch (InterruptedException retry) {
    timedOut = false;
    }
    }
    }
    ```
    nothingistrue
        12
    nothingistrue  
       2022 年 3 月 10 日
    @git00ll #11 你先看一下 BlockingQueue.take()的 Javadoc ,你这里第一条就理解错了,后面的就没法再看了。

    BlockingQueue.take()
    Retrieves and removes the head of this queue, waiting if necessary until an element becomes available.
    获取并删除队列开头元素,否则在新元素可用前一直等待
    Throws: InterruptedException - if interrupted while waiting 。
    当等待中被中断时,抛出 InterruptedException


    该方法的原理是:如果队列为空,则等待(此时线程处于 WAITING 状态,该状态时一种特殊的 BLOCKED,可以认为就是 BLOCKED,即线程阻塞),如果被(线程调度机制)唤醒,则获取并移除队列的开头元素,如果等待过程中被中断,则抛出 InterruptedException 。

    请注意:InterruptedException 是受检异常,是被捕获后应当自动纠错的异常。它的作用是告诉你线程在阻塞状态时收到了中断信号,它的意图是让你接触阻塞状态然后释放资源(当然这只是个意图,干不干取决于你,你完全可以不例会这个信号重新恢复到阻塞状态)。

    先回复一下,我先去看看 BlockingQueue.take()的源代码再说。
    git00ll
        13
    git00ll  
    OP
       2022 年 3 月 10 日
    @nothingistrue 不管怎么理解注释的描述,在 java.util.concurrent.LinkedBlockingQueue#take 方法里。第五行
    调用 takeLock.lockInterruptibly();
    点进去
    ```
    public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
    }

    public final void acquireInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
    throw new InterruptedException();
    if (!tryAcquire(arg))
    doAcquireInterruptibly(arg);
    }

    ```

    可以清楚的看到,会主动检测并清除当前线程的中断标识,如果为中断状态,清除并抛出 InterruptedException 。
    主动检测线程的中断标志位,是毫无疑问的。
    nothingistrue
        14
    nothingistrue  
       2022 年 3 月 10 日
    看了一下,不管是 lock.lockInterruptibly ,还是 condition.await ,都会在线程已经处于 interrupt 的时候抛出 InterruptedException 。但是这里的意思可能是:不允许线程不处理中断信号就走向或返回到阻塞状态。线程收到中断信号以后,可以啥也不干,但必须解除“收到中断信号”状态,类似你可以啥也不干,但必须告诉系统已经收到了。

    你必须意识到这一点,这个先中断后 take 抛出的异常,不是 take 方法想抛的,而是内部的加锁或 await 机制跑出来的。你这个异常的根源是 Thread.currentThread().interrupt();这一句,跟 take 原本的意愿没有关系。
    nothingistrue
        15
    nothingistrue  
       2022 年 3 月 10 日
    private Runnable getTask() {
    for (;;) {
    try {
    Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();
    if (r != null)
    return r;
    timedOut = true;
    } catch (InterruptedException retry) {
    timedOut = false;
    }
    }
    }

    这段代码的意思,不是忽略中断异常,它的处理跟主处理是不一样的,timedOut 一个设定为 true ,一个设定为 false 。这个是响应了 InterruptedException ,不是忽略了它。或者说,它响应了中断信号。
    nothingistrue
        16
    nothingistrue  
       2022 年 3 月 10 日
    线程的阻塞状态,Java 线程类的 interrupted 属性 /状态,这是两码事。

    另外异步任务执行调度跟线程调度也不是一码事,只有 Java 是用线程池做异步任务的,其他语言用得不是线程池。

    今天太晚了,先这样吧
    git00ll
        17
    git00ll  
    OP
       2022 年 3 月 10 日
    @nothingistrue 你发散的太多了,题目问的是 为什么 MyRun 里面设置了中断后,查询中断状态为 true ,MyRun2 中是同一个线程,再查询中断状态却为 false 。

    答案是:在线程池的 getTask 方法里,调用 BlockQueue 的 take 方法,抛中断异常后中断状态被清除了,所以 MyRun2 中获取到中断状态为 false 。
    nothingistrue
        18
    nothingistrue  
       2022 年 3 月 11 日
    @git00ll #17 请仔细看看 ThreadPoolExecutor.getTask()的 Javadoc 源码,它的作用是获取任务,不是清除中断状态,而是在获取过程中如果被中断了就自动恢复。

    try {
    Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();
    if (r != null){
    return r;
    }
    timedOut = true;
    } catch (InterruptedException retry) {
    timedOut = false;
    }
    这段代码(为了便于理解我给格式化了),结合“它在死循环中”、“有其他代码片段会使用 timeOut 这个变量”这两点后,它的作用是:

    一、当 timed =true 时,在指定的时间内从队列获取开头元素,直到获取到、超时、或者被中断,若获取到则跳出死循环执行方法返回,若超时则设置 timeOut=true 然后`continue 死循环`,若被中断则设置 timeOut=false 然后`continue 死循环`;

    二、当 timed=false 时,无限期从队列获取开头元素,直到获取到或者被中断,若获取到则跳出死循环执行方法返回,若是被中断则`continue 死循环`;

    无论哪种场景,被中断时,选择的处理都是继续循环,相当于若被中断则自动恢复。


    这里真正解释的是:为什么线程已经被标记中断了,Run2 还能被执行。

    至于你的问题,线程是何时清理中断状态的,这个问题的答案是:只要开始执行下一行代码了,它就清除中断状态了。这是一个设计理念。按照 Java 中这个 interrupted 的设计原理,当处于阻塞(等待)状态的线程,执行任何其他代码前,就得先清除中断状态,因为它只要执行了其他代码就是响应了中断信号,它就不再是“刚刚收到中断信号”的状态了。你现在是正好找到了这一处代码,但它是设计理念的果,不是因。
    nothingistrue
        19
    nothingistrue  
       2022 年 3 月 11 日
    回头看了下,我上面 #10 这段回复( Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。)是错的。纠正一下:

    Run1 后面的代码 不会清除中断状态,这实际上是会造成问题的,应该有代码手动清除(这也是当前线程调用 Thread.interrupt 在高安全策略中不被允许的一个原因)。Run1 到 Run2 之间会经过一些列的其他系统代码,这些代码只要发现中断状态就会清除它(若不清理就违反了设计理念,是 BUG )。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     984 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 52ms UTC 18:22 PVG 02:22 LAX 11:22 JFK 14:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86