V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
git00ll
V2EX  ›  Java

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

  •  
  •   git00ll · 2022-03-09 12:39:51 +08:00 · 1999 次点击
    这是一个创建于 1019 天前的主题,其中的信息可能已经有所发展或是发生改变。
        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  
       2022-03-09 15:02:32 +08:00 via iPhone
    不是有个调试工具么 jconsole
    cheng6563
        2
    cheng6563  
       2022-03-09 15:27:21 +08:00   ❤️ 1
    MyRun2 不是本来就没 interrupt 吗?
    uSy62nMkdH
        3
    uSy62nMkdH  
       2022-03-09 15:42:55 +08:00
    @cheng6563 正解
    git00ll
        4
    git00ll  
    OP
       2022-03-09 15:59:45 +08:00
    @cheng6563
    @uSy62nMkdH
    因为是个单线程的线程池,Myrun 和 Myrun2 其实用的是同一个线程。通过输出的 thread hashcode()也能看出线程是同一个。

    Myrun 里面已经将线程设为中断状态了,理论上 Myrun2 运行的时候应该是处于中断状态的,但是却没有。所以一定有一个地方将该线程的状态标识移除了,我没找到在哪里实现的。
    git00ll
        5
    git00ll  
    OP
       2022-03-09 16:07:07 +08:00
    @qfdk 这个好像不能看出线程的中断状态
    litchinn
        6
    litchinn  
       2022-03-09 17:17:49 +08:00
    `ThreadPoolExecutor` 中 `execute`有如下一段注释
    litchinn
        7
    litchinn  
       2022-03-09 17:19:04 +08:00   ❤️ 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-03-09 17:19:23 +08:00
    ```
    /**
    * 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-03-10 14:32:34 +08:00
    isInterrupted 表示的不是线程是否中断(阻塞),而是该线程是否刚收到其他线程给他的中断信号。你这里只有一个线程,没有线程之间的通信,所以 isInterrupted 应该恒定返回 false 。不过,你的 Run1 自己给自己了一个中断信号,导致 isInterrupted = true 。

    interrupt()这个方法(注意还有一个 interrupted 方法,这俩不一样)的实际作用是给目标线程一个“请你中断”的信号,并不是中断目标线程,目标线程做啥反应,是由目标线程自身决定的。Thread.currentThread().interrupt(),这样自己给自己中断信号,是一个很怪的操作。
    nothingistrue
        10
    nothingistrue  
       2022-03-10 14:38:47 +08:00
    Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。
    git00ll
        11
    git00ll  
    OP
       2022-03-10 16:50:39 +08:00
    @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-03-10 17:22:19 +08:00
    @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-03-10 17:29:33 +08:00
    @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-03-10 17:48:16 +08:00
    看了一下,不管是 lock.lockInterruptibly ,还是 condition.await ,都会在线程已经处于 interrupt 的时候抛出 InterruptedException 。但是这里的意思可能是:不允许线程不处理中断信号就走向或返回到阻塞状态。线程收到中断信号以后,可以啥也不干,但必须解除“收到中断信号”状态,类似你可以啥也不干,但必须告诉系统已经收到了。

    你必须意识到这一点,这个先中断后 take 抛出的异常,不是 take 方法想抛的,而是内部的加锁或 await 机制跑出来的。你这个异常的根源是 Thread.currentThread().interrupt();这一句,跟 take 原本的意愿没有关系。
    nothingistrue
        15
    nothingistrue  
       2022-03-10 17:52:55 +08:00
    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-03-10 17:54:34 +08:00
    线程的阻塞状态,Java 线程类的 interrupted 属性 /状态,这是两码事。

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

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

    答案是:在线程池的 getTask 方法里,调用 BlockQueue 的 take 方法,抛中断异常后中断状态被清除了,所以 MyRun2 中获取到中断状态为 false 。
    nothingistrue
        18
    nothingistrue  
       2022-03-11 09:54:18 +08:00
    @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-03-11 10:28:04 +08:00
    回头看了下,我上面 #10 这段回复( Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。)是错的。纠正一下:

    Run1 后面的代码 不会清除中断状态,这实际上是会造成问题的,应该有代码手动清除(这也是当前线程调用 Thread.interrupt 在高安全策略中不被允许的一个原因)。Run1 到 Run2 之间会经过一些列的其他系统代码,这些代码只要发现中断状态就会清除它(若不清理就违反了设计理念,是 BUG )。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2039 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 16:11 · PVG 00:11 · LAX 08:11 · JFK 11:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.