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
1
qfdk 2022-03-09 15:02:32 +08:00 via iPhone
不是有个调试工具么 jconsole
|
2
cheng6563 2022-03-09 15:27:21 +08:00 1
MyRun2 不是本来就没 interrupt 吗?
|
3
uSy62nMkdH 2022-03-09 15:42:55 +08:00
@cheng6563 正解
|
4
git00ll OP @cheng6563
@uSy62nMkdH 因为是个单线程的线程池,Myrun 和 Myrun2 其实用的是同一个线程。通过输出的 thread hashcode()也能看出线程是同一个。 Myrun 里面已经将线程设为中断状态了,理论上 Myrun2 运行的时候应该是处于中断状态的,但是却没有。所以一定有一个地方将该线程的状态标识移除了,我没找到在哪里实现的。 |
6
litchinn 2022-03-09 17:17:49 +08:00
`ThreadPoolExecutor` 中 `execute`有如下一段注释
|
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 时有重置状态操作 |
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); } ``` |
9
nothingistrue 2022-03-10 14:32:34 +08:00
isInterrupted 表示的不是线程是否中断(阻塞),而是该线程是否刚收到其他线程给他的中断信号。你这里只有一个线程,没有线程之间的通信,所以 isInterrupted 应该恒定返回 false 。不过,你的 Run1 自己给自己了一个中断信号,导致 isInterrupted = true 。
interrupt()这个方法(注意还有一个 interrupted 方法,这俩不一样)的实际作用是给目标线程一个“请你中断”的信号,并不是中断目标线程,目标线程做啥反应,是由目标线程自身决定的。Thread.currentThread().interrupt(),这样自己给自己中断信号,是一个很怪的操作。 |
10
nothingistrue 2022-03-10 14:38:47 +08:00
Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。
|
11
git00ll OP @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; } } } ``` |
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()的源代码再说。 |
13
git00ll OP @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 。 主动检测线程的中断标志位,是毫无疑问的。 |
14
nothingistrue 2022-03-10 17:48:16 +08:00
看了一下,不管是 lock.lockInterruptibly ,还是 condition.await ,都会在线程已经处于 interrupt 的时候抛出 InterruptedException 。但是这里的意思可能是:不允许线程不处理中断信号就走向或返回到阻塞状态。线程收到中断信号以后,可以啥也不干,但必须解除“收到中断信号”状态,类似你可以啥也不干,但必须告诉系统已经收到了。
你必须意识到这一点,这个先中断后 take 抛出的异常,不是 take 方法想抛的,而是内部的加锁或 await 机制跑出来的。你这个异常的根源是 Thread.currentThread().interrupt();这一句,跟 take 原本的意愿没有关系。 |
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 ,不是忽略了它。或者说,它响应了中断信号。 |
16
nothingistrue 2022-03-10 17:54:34 +08:00
线程的阻塞状态,Java 线程类的 interrupted 属性 /状态,这是两码事。
另外异步任务执行调度跟线程调度也不是一码事,只有 Java 是用线程池做异步任务的,其他语言用得不是线程池。 今天太晚了,先这样吧 |
17
git00ll OP @nothingistrue 你发散的太多了,题目问的是 为什么 MyRun 里面设置了中断后,查询中断状态为 true ,MyRun2 中是同一个线程,再查询中断状态却为 false 。
答案是:在线程池的 getTask 方法里,调用 BlockQueue 的 take 方法,抛中断异常后中断状态被清除了,所以 MyRun2 中获取到中断状态为 false 。 |
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 的设计原理,当处于阻塞(等待)状态的线程,执行任何其他代码前,就得先清除中断状态,因为它只要执行了其他代码就是响应了中断信号,它就不再是“刚刚收到中断信号”的状态了。你现在是正好找到了这一处代码,但它是设计理念的果,不是因。 |
19
nothingistrue 2022-03-11 10:28:04 +08:00
回头看了下,我上面 #10 这段回复( Run1 后面的代码不会清除中断状态,如果没有其他东西干涉,这俩 isInterrupted 要都是 true 。但是上面的情况对 Run2 不合理,所以肯定要有啥东西来做干涉,这事前面的人回复的更好。)是错的。纠正一下:
Run1 后面的代码 不会清除中断状态,这实际上是会造成问题的,应该有代码手动清除(这也是当前线程调用 Thread.interrupt 在高安全策略中不被允许的一个原因)。Run1 到 Run2 之间会经过一些列的其他系统代码,这些代码只要发现中断状态就会清除它(若不清理就违反了设计理念,是 BUG )。 |