Java是一门原生支持多线程的语言,要开启一个线程很容易,使用如下代码
class="java">new Thread(new Runnable() { @Override public void run() { //do something } }).start();
?这是我能想到的启动线程的最简单的代码,语义明确.但是要优雅得关闭该线程通常却很难.
?
API中有Thread.stop()这个方法.但是由于各种原因该方法已经被标记为Deprecated所以一定不要使用该方法停止线程运行.正确的方法是使用Thread.isInterrupted()和Thread.interrupt(),组合使用这两个方法来中断一个线程.我对这两个方法的理解是,前者检查一个中断标记,后者设置一个中断标记.程序通过在合适的时机检查中断标记来合理的停止任务的执行.这两个方法除了对中断标记做读写之外不影响程序的正常运行,也就是说,无论如何调用interrupt方法,程序始终在自己的控制中.另外还有一个方法interrupted这个方法跟isInterrupted得用相同都是检查中断标记,但它还有另外一个副作用,检查标记后会重置标记为false.就是说连续调用两次该方法第二次调用一定会返回false.
?
下面看一个run方法的实现
@Override public void run() { while (true) { boolean shouldStop = doSomething();//执行具体的任务,并返回是否应当停止运行 if (shouldStop) break; } }
?对于如此实现run方法的Runnable可以不行可任何特别的处理,按正常逻辑执行完,run方法会自然返回,线程也就终止运行了.但是如果想在线程外强制停止线程运行呢?例如有如下的main方法.
?
public static void main(String[] args) throws InterruptedException { Thread task = new Thread(new Runnable() { @Override public void run() { while (Thread.currentThread().isInterrupted()) { boolean shouldStop = doSomething(); if (shouldStop) break; } } }); task.start(); Thread.sleep(5000); //睡眠一段时间后在此位置 task.interrupt(); }
通过设置循环条件为检查当前线程的中断标记,来控制是否跳出循环,此时每次执行完doSomething都会检查一次中断标记.由于程序在主线程睡眠5秒后会设置task的中断标记为true,所以线程会在5秒多一点的时间后停止.
假设Java虚拟机如果不是如此实现,而是在interrupt的时候直接通过系统底层实现强制终止线程,这时doSomething可能正在像硬盘上面写一段数据,还没写完,突然被停掉了,这会导致数据损坏,这种方式是无脑的,所以通过中断标记的机制让程序自己决定是否要退出是理智的.
?
但是如果doSomething中执行了一个会阻塞线程的操作时会怎么样呢?重新审视上面的代码.
public static void main(String[] args) throws InterruptedException { Thread task = new Thread(new Runnable() { @Override public void run() { while (Thread.currentThread().isInterrupted()) {//1 boolean shouldStop = doSomething();//2,阻塞等待键盘输入 if (shouldStop) break; } } }); task.start(); Thread.sleep(5000); //睡眠一段时间后在此位置 task.interrupt();//3 }
可以想想如果doSomething中在阻塞等待一个键盘输入,5秒后线程中断标记被设置为true,也就是程序按1,2,3的顺序执行了代码.由于2导致线程阻塞,除非有一个键盘输入,否则线程将一直被doSomething阻塞,检查中断标记的语句1得不到执行,此时上面的机制失效.下面具体分析阻塞线程的中断问题.
?
对于线程中的阻塞操作可以分为两种
1.可中断阻塞
这类阻塞操作一般会在线程中断标记被设置为true的时候,抛出异常以停止阻塞,看Thread.sleep()的方法声明.
public static void sleep(long millis, int nanos) throws InterruptedException
?从该声明中可以看到,该方法在其他线程设置该线程的中断标记为true时,会抛出该异常,抛出该异常后会清除当前的中断标记(重置为false)
?
JSE中除了Thread.sleep有许多这样的方法声明.例如LinkedBlockingQueue中的take,put方法.
对于此类中断操作可以做如下代码中的处理
?
public static void main(String[] args) throws InterruptedException { Thread task = new Thread(new Runnable() { @Override public void run() { while (Thread.currentThread().isInterrupted()) { try { //假设doSomething是一个会抛出InterruptedException的阻塞操作 boolean shoudStop = doSomething(); if (shoudStop) break; } catch (InterruptedException e) { //由于抛出该异常会重置中断标记 //所以捕获该异常后需要重新设置中断标记以中断线程 Thread.currentThread().interrupt(); } } } }); task.start(); Thread.sleep(5000); //睡眠一段时间后在此位置 //导致doSomething抛出InterruptedException task.interrupt(); }
?
?
2.不可中断的阻塞操作
?这类操作对中断标记的设置毫无反应,会顽固的阻塞下去.此类异常一般为IO操作,对于这样的IO阻塞操作一般会在底层资源被关闭(释放)时抛出IOException.例如InputStream的read操作.参考如下代码的处理.
public static void main(String[] args) throws Exception { Thread task = new Thread(new Runnable() { @Override public void run() { while (Thread.currentThread().isInterrupted()) { try { //阻塞等待标准输入中读取一个字节 int data = System.in.read(); if (data == -1) break; } catch (IOException e) { e.printStackTrace(); //由于抛出该异常不会重置中断标记 //所以捕获该异常后不许需要重新设置中断标记 //直接退出循环即可 break; } } } }); task.start(); Thread.sleep(5000); //睡眠一段时间后在此位置 task.interrupt(); //关闭(释放)标准输入流,导致System.in.read()抛出IOException System.in.close(); }
?看起来也不难,但是实际情况通常比较负责,特别是有些IO操作的close方法本身就有可能阻塞.可以看一下BufferedReader的源代码.
?
?
public class BufferedReader extends Reader { ........//省略部分代码 public int read() throws IOException { synchronized (lock) { ensureOpen(); for (;;) { if (nextChar >= nChars) { fill(); if (nextChar >= nChars) return -1; } if (skipLF) { skipLF = false; if (cb[nextChar] == '\n') { nextChar++; continue; } } return cb[nextChar++]; } } } public void close() throws IOException { synchronized (lock) { if (in == null) return; in.close(); in = null; cb = null; } } }
?可以看到read方法和close方法都需要对lock加锁,进行同步.如果线程整阻塞在read操作上,此时read持有lock锁.其他线程调用close时由于无法获取lock锁,也将阻塞,导致死锁.考虑如下代码
public static void main(String[] args) throws Exception { final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); Thread task = new Thread(new Runnable() { @Override public void run() { while (Thread.currentThread().isInterrupted()) { try { //阻塞等待标准输入中读取一个字节 int data = reader.read(); if (data == -1) break; } catch (IOException e) { e.printStackTrace(); //由于抛出该异常不会重置中断标记 //所以捕获该异常后不许需要重新设置中断标记 //直接退出循环即可 break; } } } }); task.start(); Thread.sleep(5000); //睡眠一段时间后在此位置 task.interrupt(); //关闭(释放)reader,死锁.close无法执行完成. reader.close(); //直接调用底层资源的close方法即可使阻塞的read操作抛出IOException //所以此处应替换为如下注释中的代码 //System.in.close(); }