本文转自:Java 线程阻塞、中断及优雅退出
一、线程阻塞
一个线程进入阻塞状态的原因可能如下(已排除Deprecated方法):
sleep()
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象锁并没有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
wait()
调用wait()/1.5中的condition.await()使线程挂起,直到线程获取notify()/notifyAll()消息,(或者在Java SE5中java.util.concurrent类库中等价的signal()/signalAll()消息),线程才会进入就绪状态;
wait()调用会释放当前对象锁(monitor),这样其他线程可以继续进入对象的同步方法。参见上一篇文章线程间协作——wait & notify & notifyAll
另外,调用join()也会导致线程阻塞,因为源码中join()就是通过wait()实现的;
等待I/O
class Demo3 implements Runnable throws InterruptedException{
private InputStream in;
public void run(){
in.read();
}
}
无法持有锁进入同步代码
进入同步代码前无法获取锁,比如试图调用synchronized方法,或者显示锁对象的上锁行为ReentrantLock.lock(),而对应锁已被其他线程获取的情况下都将导致线程进入阻塞状态;
注意:yield()并不会导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
二、线程中断
线程中断可以在线程内部设置一个中断标识,同时让处于(可中断)阻塞的线程抛出InterruptedException中断异常,使线程跳出阻塞状态。相比其他语言,Java线程中断比较特殊,经常会引起开发人员的误解。因为中断听起来高深复杂,实质原理上非常简单。
中断原理
Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
Java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
中断相关的方法
方法 | 解释 |
---|---|
public static boolean interrupted() | 测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外) |
public boolean isInterrupted() | 测试线程是否已经中断。线程的中断状态不受该方法的影响 |
public void interrupt() | 中断线程,设置中断标识为为true |
其中,interrupt方法是唯一能将中断状态设置为true的方法。静态方法interrupted会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。
此外,类库中的有些类的方法也可能会调用中断,如FutureTask中的cancel方法,如果传入的参数为true,它将会在正在运行异步任务的线程上调用interrupt方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么cancel方法中的参数将不会起到什么效果;
ExecutorService exec = Executors.newCachedThreadPool();
Futrue<?> f = exec.submit(new TaskThread());
f.interrupt();
又如ThreadPoolExecutor中的shutdownNow方法会遍历线程池中的工作线程并调用线程的interrupt方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++)
exec.execute(new TaskThread())
exec.shutdownNow();
中断的处理 - 处理 InterruptedException
如果抛出 InterruptedException 意味着一个方法是阻塞方法,那么调用一个阻塞方法则意味着您的方法也是一个阻塞方法,而且您应该有某种策略来处理 InterruptedException。通常最容易的策略是自己抛出 InterruptedException,如清单 1 中 putTask() 和 getTask() 方法中的代码所示。 这样做可以使方法对中断作出响应,并且只需将 InterruptedException 添加到 throws 子句。
清单 1. 不捕捉 InterruptedException,将它传播给调用者
public class TaskQueue {
private static final int MAX_TASKS = 1000;
private BlockingQueue<Task> queue
= new LinkedBlockingQueue<Task>(MAX_TASKS);
public void putTask(Task r) throws InterruptedException {
queue.put(r);
}
public Task getTask() throws InterruptedException {
return queue.take();
}
}
有时候需要在传播异常之前进行一些清理工作。在这种情况下,可以捕捉 InterruptedException,执行清理,然后抛出异常。清单 2 演示了这种技术,该代码是用于匹配在线游戏服务中的玩家的一种机制。 matchPlayers() 方法等待两个玩家到来,然后开始一个新游戏。如果在一个玩家已到来,但是另一个玩家仍未到来之际该方法被中断,那么它会将那个玩家放回队列中,然后重新抛出 InterruptedException,这样那个玩家对游戏的请求就不至于丢失。
清单 2. 在重新抛出 InterruptedException 之前执行特定于任务的清理工作
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
try {
Player playerOne, playerTwo;
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
不要生吞中断
有时候抛出 InterruptedException 并不合适,例如当由 Runnable 定义的任务调用一个可中断的方法时,就是如此。在这种情况下,不能重新抛出 InterruptedException,但是您也不想什么都不做。当一个阻塞方法检测到中断并抛出 InterruptedException 时,它清除中断状态。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应。该任务可以通过调用 interrupt() 以 “重新中断” 当前线程来完成,如清单 3 所示。至少,每当捕捉到 InterruptedException 并且不重新抛出它时,就在返回之前重新中断当前线程。
清单 3. 捕捉 InterruptedException 后恢复中断状态
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
当线程处于阻塞状态时,中断线程,抛出InterruptedException,此时线程并未中断,而是再次唤醒,所以需要在异常中再次设置标志位进行中断操作。(举个例子就是:你在睡觉,有人把你叫醒去做其他事,你醒后脑袋懵的,不知道做什么,需要人再次叫你去做相应的事情)
处理 InterruptedException 时采取的最糟糕的做法是生吞它 —— 捕捉它,然后既不重新抛出它,也不重新断言线程的中断状态。对于不知如何处理的异常,最标准的处理方法是捕捉它,然后记录下它,但是这种方法仍然无异于生吞中断,因为调用栈中更高层的代码还是无法获得关于该异常的信息。(仅仅记录 InterruptedException 也不是明智的做法,因为等到人来读取日志的时候,再来对它作出处理就为时已晚了。) 清单 4 展示了一种使用得很广泛的模式,这也是生吞中断的一种模式:
清单 4. 生吞中断 —— 不要这么做
// Don't do this
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException swallowed) {
/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
}
}
}
如果不能重新抛出 InterruptedException,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。标准线程池 (ThreadPoolExecutor)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知执行线程线程池正要关闭。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。
中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求,这种线程的run方法遵循如下形式:
public void run() {
try {
...
/*
* 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
* !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
* 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
*/
while (!Thread.currentThread().isInterrupted()&& more work to do) {
do more work
}
} catch (InterruptedException e) {
//线程在wait或sleep期间被中断了
} finally {
//线程结束前做一些清理工作
}
}
上面是while循环在try块里,如果try在while循环里时,因该在catch块里重新设置一下中断标示,因为抛出InterruptedException异常后,中断标示位会自动清除,此时应该这样:
public void run() {
while (!Thread.currentThread().isInterrupted()&& more work to do) {
try {
...
sleep(delay);
//wait(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); //重新设置中断标示
}
}
}
三、可中断阻塞与不可中断阻塞
对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。
不可中断的操作,包括获取锁(进入synchronized段)以及Lock.lock(),Java.io 包中的同步 I/O,Java.io 包中的同步 Socket IO (对套接字进行读取和写入的操作:InputStream 和 OutputStream 中的read和write等),Java.nio包中的Selector的异步I/O 等。调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
但是,你可能正使用Java1.0之前就存在的传统的I/O,Thread.interrupt()将不起作用,因为线程将不会退出被阻塞状态。
很幸运,对于 Socket 同步 I/O,Java平台为这种情形提供了一项解决方案,即调用阻塞该线程的套接字的close()方法。在这种情形下,如果线程被I/O操作阻塞,当调用该套接字的close方法时,该线程在调用accept地方法将接收到一个SocketException(SocketException为IOException的子异常)异常,这与使用interrupt()方法引起一个InterruptedException异常被抛出非常相似。
java.nio类库提供了更加人性化的I/O中断,被阻塞的nio通道会自动地响应中断,不需要关闭底层资源;
对于非标准的取消操作,我们可以一些方法来对它进行封装(如通过改写interrupt方法来将它封装在 Thread 中,通过 newTaskFor 等)。
改写 interrupt 方法封装 Socket 非标准的取消方式
下面的例子将通过改写 interrupt 方法将 Socket 非标准的取消方式封装在 Thread 中。封装之后,调用者可以通过调用 interrupt 方法来取消Socket 操作。
public class ReaderThread extends Thread {
private static final int BUFSZ = 512;
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
@Override
public void interrupt() {
// 先关闭套接字,再调用 interrupt
try {
socket.close();
} catch (IOException ignored) {
} finally {
super.interrupt();
}
}
public void run() {
try {
byte[] buf = new byte[BUFSZ];
while(true) {
int count = in.read(buf);
if(count < 0) {
break;
} else if (count > 0) {
processBuffer(buf, count);
}
}
} catch (IOException e) {
/* Allow thread to exit */
interrupt();
}
}
public void processBuffer(byte[] buf, int count) {
}
}
采用 newTaskFor 来封装非标准的取消
我们可以通过 newTaskFor 方法来进一步优化 ReaderThread 中封装非标准取消的技术,这是 Java 6 在 ThreadPoolExecutor 中的新增功能。当把一个 Callable 提交给 ExecutorService 时,submit 方法会返回一个 Future,我们可以通过这个 Future 来取消任务。newTaskFor 是一个工厂方法,它将创建 Future 来代表任务。newTaskFor 还能返回一个 RunnableFuture 接口,该接口扩展了 Future 和 Runnable(并由 Future 实现)。
通过定制表示任务的 Future 可以改变 Future.cancel 的行为。例如,定制的取消代码可以实现日志记录或者手机取消操作的统计消息,以及取消一些不响应中断的操作。通改写 interrupt 方法,ReaderThread 可以取消基于套接字的线程。同样,通过改写 cancel 方法也可以实现类似的功能。
定义可取消任务
接口
import java.util.concurrent.Callable;
import java.util.concurrent.RunnableFuture;
public interface CancellableTask<T> extends Callable<T> {
void cancel();
RunnableFuture<T> newTask();
}
实现类
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import com.johnfnash.learn.annotation.GuardedBy;
public class SocketUsingTask<T> implements CancellableTask<T> {
@GuardedBy("this")
private Socket socket;
public synchronized void setSocket(Socket s) {
this.socket = s;
}
@Override
public T call() throws Exception {
// ......
return null;
}
@Override
public synchronized void cancel() {
try {
if(socket != null) {
socket.close();
}
} catch (IOException ignored) {
}
}
// 改写 newTask 中返回的 RunnableFuture 的 cancel 方法,添加调用 CancellableTask 自身的 cancel 方法来关闭套接字
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this) {
@SuppressWarnings("finally")
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
try {
SocketUsingTask.this.cancel();
} finally {
return super.cancel(mayInterruptIfRunning);
}
}
};
}
}
SocketUsingTask 实现了 CancellableTask,并定义了 Future.cancel 来关闭套接字和调用super.cancel。如果 SocketUsingTask 通过自己的 Future 来取消,那么底层的套接字将被关闭,并且线程将被中断。因此它提高了任务对取消操作的响应性:不仅能够在调用可中断方法的同时确保响应取消操作,而且还能调用可阻塞的套接字 I/O 方法。
扩展线程池
扩展线程池,改写 newTaskFor 方法,当传入的 Callable 任务为 CancellableTask类型时,直接使用自身的newTaskFor 方法来创建 RunnableFuture。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.johnfnash.learn.annotation.ThreadSafe;
@ThreadSafe
public class CancellationExecutor extends ThreadPoolExecutor {
public CancellationExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
if(callable instanceof CancellableTask) {
return ((CancellableTask<T>)callable).newTask();
} else {
return super.newTaskFor(callable);
}
}
}
四、线程优雅退出
一般情况下,线程退出可以使用while循环判断共享变量条件的方式,当线程内有阻塞操作时,可能导致线程无法运行到条件判断的地方而导致一直阻塞下去,这个时候就需要中断来帮助线程脱离阻塞。因此比较优雅的退出线程方式是结合共享变量和中断。
thread = new Thread(new Runnable() {
@Override
public void run() {
/*
* 在这里为一个循环,条件是判断线程的中断标志位是否中断
*/
while (flag&&(!Thread.currentThread().isInterrupted())) {
try {
Log.i("tag","线程运行中"+Thread.currentThread().getId());
// 每执行一次暂停40毫秒
//当sleep方法抛出InterruptedException 中断状态也会被清掉
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
//如果抛出异常则再次设置中断请求
Thread.currentThread().interrupt();
}
}
}
});
thread.start();
参考
4.《Java 编程思想》