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

2022-03-09 12:39:51 +08:00
 git00ll
    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

2002 次点击
所在节点    Java
19 条回复
qfdk
2022-03-09 15:02:32 +08:00
不是有个调试工具么 jconsole
cheng6563
2022-03-09 15:27:21 +08:00
MyRun2 不是本来就没 interrupt 吗?
uSy62nMkdH
2022-03-09 15:42:55 +08:00
@cheng6563 正解
git00ll
2022-03-09 15:59:45 +08:00
@cheng6563
@uSy62nMkdH
因为是个单线程的线程池,Myrun 和 Myrun2 其实用的是同一个线程。通过输出的 thread hashcode()也能看出线程是同一个。

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

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

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

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

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

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

Run1 后面的代码 不会清除中断状态,这实际上是会造成问题的,应该有代码手动清除(这也是当前线程调用 Thread.interrupt 在高安全策略中不被允许的一个原因)。Run1 到 Run2 之间会经过一些列的其他系统代码,这些代码只要发现中断状态就会清除它(若不清理就违反了设计理念,是 BUG )。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/839112

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX