大多时候线程是运行直到结束或者让他们自己停止。然而有时候我们希望提前结束任务或线程,或许是因为用户取消的操作,或者应用程序需要被快速关闭。
java中没有提供任何机制来安全地终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
这种协作方式是必要的,我们很少希望某个任务,线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这就提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清除如何执行清除工作。
一个行为良好的软件与勉强运行的软件之间最主要区别就是,行为良好的软件能很完善地处理失败,关闭的取消等过程。
我们来看一下以下的这几种停止线程的方式
1.任务取消
在java中没有一种安全的抢占方式停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
其中一种协作机制就是设置某个"已请求取消"标志,而任务将定期检查该标志。如果设置了这个标志,那么任务将提前结束。
代码示例如下:
搜索素数
public class PrimeGenerator implements Runnable{
private static ExecutorService exec = Executors.newCachedThreadPool();
private final List<BigInteger> primes = new ArrayList<>();
//注意:这里的cancelled要用volatile修饰,以便于保证从主内存加载到线程工作内存的值是最新的
private volatile boolean cancelled;
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
cancelled = true;
}
public synchronized List<BigInteger> get(){
return new ArrayList<>(primes);
}
public static void main(String[] args) throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
exec.execute(generator);
try {
SECONDS.sleep(1);
}finally {
generator.cancel();
}
List<BigInteger> list = generator.get();
for (BigInteger i : list) {
System.out.println(i);
}
}
}
2.通常来说中断是实现取消的最合理的方式
上面的示例中存在着这样的问题:
搜索素数的线程退出需要花费一定的时间,如果使用这种方法的任务调用了一个阻塞方法,例如:BlockingQueue.put,那么可能会产生一个更严重的问题–任务可能永远不会检查取消标志,因此永远不会结束。
对中断的理解:
中断并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被成为取消点).有些方法,例如wait,sleep,和join等,将严格地处理这种请求,当他们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。
public void interrupt() { ... } //中断目标线程
public boolean isInterrupted{ ... } //返回目标线程的中断状态
public static boolean interrupted(){ ... } // 清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方式。
以下是一个生产素数的生产者代码的实现
public class PrimeProducer extends Thread {
private final BlockingDeque<BigInteger> queue;
public PrimeProducer(BlockingDeque<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
//检查当前线程是否被设置中断**
while (!Thread.currentThread().isInterrupted()) {
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException consumed) {
// 允许线程退出
}
}
public void cancel() {
//中断线程
interrupt();
}
}
3.处理不可中断的阻塞
并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有任何其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但是这要求我们必须知道线程阻塞的原因。
如下示例 通过改写interrupt方法将非标准的取消操作封装在Thread中
//处理不可中断阻塞
//通过改写interrupt方法将非标准的取消操作封装到Thread中
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() {
try {
socket.close();
} catch (IOException e) {
}finally {
super.interrupt();
}
}
@Override
public void run() {
byte[] buf = new byte[BUFSZ];
while (true) {
try {
int count = in.read(buf);
if (count < 0) {
break;
}else if (count > 0) {
processBuffer(buf,count);
}
} catch (IOException e) {
// TODO Auto-generated catch block
//允许线程退出
}
}
}
public void processBuffer(byte[] buff,int count) {
}
}
4.停止基于线程的服务
我们在实现多线程服务时,一般使用线程池,这些服务的生命周期通常比创建线程的方法生命周期更长。如果程序准备退出,那么这些服务所拥有的线程也应该结束。
线程由Thread表示,并且像其他对象一样可以被自由共享。注意:线程由一个对应的所有者,即创建该线程的类。因此线程池是其工作线程的所有者,如果要中断这些线程,那么应该使用线程池。
所以:对于持有线程的服务,只要服务的存在时间大于创建线程方法的存在时间,那么就应该提供生命周期方法来关闭它自己以及它所拥有的线程。
4.1关闭ExecutorService
shutdown 正常关闭(会等队列中的所有任务都执行完成后才关闭)
shutdownNow 强行关闭(会首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单)
4.2 “毒丸” 对象
“毒丸”是指一个放在队列上的对象,其含义:“当得到这个对象时”,立即停止.
这种方法关闭生产者-消费者服务的方式只能在已经知道生产者和消费者的数量的情况下使用。
具体代码如下:
public class IndexingService {
private static final int CAPACITY = 1000;
private static final File POSION = new File(""); //充当毒丸
private final IndexerThread consumer = new IndexerThread();
private final CrawlerThread producer = new CrawlerThread();
private final BlockingQueue<File> queue;
private final FileFilter fileFilter;
private final File root;
public IndexingService(File root, final FileFilter fileFilter) {
// TODO Auto-generated constructor stub
this.root = root;
this.queue = new LinkedBlockingQueue<>(CAPACITY);
this.fileFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
// TODO Auto-generated method stub
return pathname.isDirectory() || fileFilter.accept(pathname);
}
};
}
public void start() {
producer.start();
consumer.start();
}
public void stop() {
producer.interrupt();
}
public void awaitTermination() throws InterruptedException {
consumer.join();
}
private boolean alreadyIndexed(File f) {
return false;
}
// 生产者
class CrawlerThread extends Thread {
@Override
public void run() {
try {
crawl(root);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
while (true) {
try {
queue.put(POSION);
break;
} catch (InterruptedException e) {
//retry
}
}
}
}
private void crawl(File root) throws InterruptedException {
File[] entries = root.listFiles(fileFilter);
if (entries != null) {
for (File entry : entries) {
if (entry.isDirectory()) {
crawl(entry);
} else if (!alreadyIndexed(entry)) {
queue.put(entry);
}
}
}
}
}
// 消费者
class IndexerThread extends Thread {
@Override
public void run() {
while (true) {
try {
File file = queue.take();
if (file == POSION) {
break;
} else {
indexFile(file);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void indexFile(File file) {
/**
* 一些操作
*/
}
}
}