Java并发编程(六):任务取消、中断与关闭

这一节介绍并发编程中如何对任务执行取消操作。java中没有提供停止线程的方法,只用通过协作——使代码遵循一定的协议来请求取消。

轮询检查

一种常见的取消任务的办法是轮询检查。线程在执行的过程中通过定期检查一个boolean变量来决定是否终止任务。下面是一个使用轮询检查来终止素数生成的例子:

public class PrimeGenerator implements Runnable {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    private final List<BigInteger> primes
            = new ArrayList<BigInteger>();
    private volatile boolean cancelled;

    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<BigInteger>(primes);
    }
}

但是轮询检查也存在问题,比如在循环中调用一个阻塞方法,如BlockingQueue.put,如果该方法阻塞,则意味着在阻塞结束之前都不会进入下一轮循环,也就不会再检查boolean变量了,因而阻塞期间无法终止该任务。

中断

对于上面的问题,更常用的方法是使用中断(interrupt)机制。在java中,每一个线程都有一个中断标识,一个线程t1调用另一个线程t2的interrupt方法,是将t2的中断标识设置成了true,使t2变成中断状态,但对于t2是否终止自己的任务,由t2自己决定。至于线程如何终止当前任务,这与具体场景和业务逻辑相关。
下面是一个对阻塞方法使用中断取消任务的例子:

public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            //允许线程退出
        }
    }

    public void cancel() {
        interrupt();
    }
}

在这个例子中,在每次执行循环之前都会判断线程是否处于阻塞状态,若是则不会再执行任务。

InterruptException

在上面的例子中,方法会捕获一个InterruptException异常,那么什么时候会引发InterruptException呢?当一个线程处于中断状态(isInterrupted返回true)时调用了导致阻塞的方法(如sleep,join,wait),或者一个线程阻塞期间被interrupt导致中断,就会引发InterruptException,InterruptException被抛出后线程的中断状态会被消除(重置为false)。

对于InterruptException,有两种处理办法,一种是将其传递给调用者(在方法上使用throws);二是将其捕获并且恢复中断状态(Thead.currentThread.interrupt()),并根据中断策略进行处理。一般来说不应该掩盖InterruptException,就是在捕获之后什么也不做,除非我们知道这么做不会引起任何不良后果。

如果不清楚一个线程的中断策略,就不要调用interrupt方法,因为我们不知道有什么任务在运行、线程在被中断之后又做了什么。

常用的捕获异常的代码:

		try {
            task.get(timeout, unit);
        } catch (TimeoutException e) {
            // 之后任务会被取消
        } catch (ExecutionException e) {
            // 任务执行过程中引发的异常,将其重新抛出
            throw launderThrowable(e.getCause());
        } finally {
            //当任务已经结束可以这么做
            task.cancel(true);
        }

不可中断阻塞

对于一些不能响应中断的阻塞如socket IO、等待获取内部锁等,一种办法是重写interrupt方法 ,然后在interrupt方法中执行取消操作。下面是一个取消socket IO的例子,通过重写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();
    }

    public void 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) { 
        	/* 允许线程退出 */
        }
    }

    public void processBuffer(byte[] buf, int count) {
    }
}

停止基于线程的服务

除非拥有某个线程,否则不应该取消或操纵它。线程的拥有者就是创建线程的类,当使用线程池时,停止线程应该由线程池负责。
在应用程序中,包含多个服务,而服务可能拥有工作线程。应用程序不应该直接关闭线程,正确的做法是应用程序关闭服务,由服务提供生命周期方法来终止线程。

实例:日志服务的关闭
这里实现了一个多生产者-但消费者的日志服务,有多个应用程序(生产者线程)将日志消息提交进队列,由一个消费者线程从队列中取出消息并打印输出。常用的关闭这种服务的做法是设置一个bool变量,当提交消息时检查该变量,若为true则通过抛出异常来终止当前生产者线程的提交操作;而消费者线程会不断处理消息直至队列为空。由于存在判断bool变量这一竞争条件,这就要求操作是原子的,而同时我们又希望不在插入队列的操作上加锁(这样多个生产者可以并发提交消息,不必获得锁),这通过更细粒度的锁来实现,代码:

public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    private boolean isShutdown;
    private int reservations;

    public LogService(Writer writer) {
        this.queue = new LinkedBlockingQueue<String>();
        this.loggerThread = new LoggerThread();
        this.writer = new PrintWriter(writer);
    }

    public void start() {
        loggerThread.start();
    }
    
    public void stop() {
        synchronized (this) {
            isShutdown = true;
        }
        loggerThread.interrupt();
    }

    public void log(String msg) throws InterruptedException {
        synchronized (this) {
            if (isShutdown)
                throw new IllegalStateException(/*...*/);
            ++reservations;
        }
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        public void run() {
            try {
                while (true) {
                    try {
                        synchronized (LogService.this) {
                            if (isShutdown && reservations == 0)
                                break;
                        }
                        String msg = queue.take();
                        synchronized (LogService.this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) { 
                    	/* 重试 */
                    }
                }
            } finally {
                writer.close();
            }
        }
    }
}

致命药丸(poison pill)

致命药丸是一种关闭线程的机制:它是一个可识别的特殊的对象,置于队列中,工作者线程在获取该对象之后停止工作。当致命药丸应用于生产者-消费者模式时,生产者在提交了致命药丸之后不会再提交任何工作;消费者在获取致命药之后不会继续获取工作。致命药丸只有在生产者、消费者数量已知的情况下才能使用,例如有N个生产者和1个消费者,则消费者再接收了N个药丸时停止;反过来如果有1个生产者和N个消费者,则生产者需要向队列中放置N个药丸来停止任务。致命药丸通常用于无限队列中。下面的例子使用致命药丸来实现桌面文件搜索:

public class IndexingService {
    private static final int CAPACITY = 1000;
    private static final File POISON = 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) {
        this.root = root;
        this.queue = new LinkedBlockingQueue<File>(CAPACITY);
        this.fileFilter = new FileFilter() {
            public boolean accept(File f) {
                return f.isDirectory() || fileFilter.accept(f);
            }
        };
    }

    private boolean alreadyIndexed(File f) {
        return false;
    }

    //生产者线程
    class CrawlerThread extends Thread { 
        public void run() {
            try {
                crawl(root);
            } catch (InterruptedException e) { /* fall through */
            } finally {
                while (true) {
                    try {
                        queue.put(POISON);
                        break;
                    } catch (InterruptedException e1) { /* 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 {
        public void run() {
            try {
                while (true) {
                    File file = queue.take();
                    if (file == POISON)
                        break;
                    else
                        indexFile(file);
                }
            } catch (InterruptedException consumed) {
            }
        }

        public void indexFile(File file) {
            /*...*/
        };
    }

    public void start() {
        producer.start();
        consumer.start();
    }

    public void stop() {
        producer.interrupt();
    }

    public void awaitTermination() throws InterruptedException {
        consumer.join();
    }
}

处理反常的线程终止

线程执行过程中抛出RuntimeException,或者其他未受检异常,导致线程死亡。这时我们需要通知框架,之后由框架根据需求是否创建新线程来取代该线程的工作。例:

public void run(){    Throwable thrown = null;
    try{
        while(!isInterrupted())
            runTask(getTaskFromWorkQueue);
    }catch(Throwable e){
        thrown = e;
    }finally{
        threadExited(this, thrown);
    }
}

处理未捕获的异常

对于未捕获的异常,通常需要将错误信息写入日志,这通过实现UncaughtExceptionHandler接口来实现。例:

public class UEHLogger implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
    }
}

参考文献:《Java并发编程实战》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值