Java并发编程实战 取消与关闭总结

任务取消
如果外部代码能在某个操作正常完成之前将其置入 完成 状态 那么这个操作就可以称为可取消的(Cancellable) 取消某个操作的原因很多:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序事件
  • 错误
  • 关闭

在Java中没有一种安全的抢占式方法来停止线程 因此也就没有安全的抢占式方法来停止任务 只有一些协作式的机制 使请求取消的任务和代码都遵循一种协商好的协议

其中一种协作机制能设置某个 已请求取消(Cancellation Requested) 标志 而任务将定期地查看该标志 如果设置了这个标志 那么任务将提前结束

使用volatile类型的域来保存取消状态

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

    @GuardedBy("this") 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);
    }

    static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        exec.execute(generator);
        try {
            SECONDS.sleep(1);
        } finally {
            generator.cancel();
        }
        return generator.get();
    }
}

PrimeGenerator使用了一种简单的取消策略:客户代码通过调用cancel来请求取消 PrimeGenerator在每次搜索素数前首先检查是否存在取消请求 如果存在则退出

中断
PrimeGenerator中的取消机制最终会使得搜索素数的任务退出 但在退出过程中需要花费一定的时间 然而 如果使用这种方法的任务调用了一个阻塞方法 例如BlockingQueue.put 那么可能会产生一个更严重的问题 任务可能永远不会检查取消标志 因此永远不会结束

不可靠的取消操作将把生产者置于阻塞的操作中(不要这么做)

class BrokenPrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    private volatile boolean cancelled = false;

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

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!cancelled)
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
        }
    }

    public void cancel() {
        cancelled = true;
    }
}

在Java的API或语言规范中 并没有将中断与任何取消语义关联起来 但实际上 如果在取消之外的其他操作中使用中断 那么都是不合适的 并且很难支撑起更大的应用

每个线程都有一个boolean类型的中断状态 当中断线程时 这个线程的中断状态将被设置为true 在Thread中包含了中断线程以及查询线程中断状态的方法 interrupt方法能中断目标线程 而isInterrupted方法能返回目标线程的中断状态 静态的interrupted方法将清除当前线程的中断状态 并返回它之前的值 这也是清除中断状态的唯一方法

调用interrupt并不意味着立即停止目标线程正在进行的工作 而只是传递了请求中断的消息

通常 中断是实现取消的最合理方式

通过中断来取消

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) {
            /* Allow thread to exit */
        }
    }

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

中断策略
正如任务中应该包含取消策略一样 线程同样应该包含中断策略 中断策略规定线程如何解释某个中断请求 当发现中断请求时 应该做哪些工作(如果需要的话) 哪些工作单元对于中断来说是原子操作 以及以多快的速度来响应中断

最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出 在必要时进行清理 通知某个所有者该线程已经退出 此外还可以建立其他的中断策略 例如暂停服务或重新开始服务 但对于那些包含非标准中断策略的线程或线程池 只能用于能知道这些策略的任务中

由于每个线程拥有各自的中断策略 因此除非你知道中断对该线程的含义 否则就不应该中断这个线程

响应中断
当调用可中断的阻塞函数时 例如Thread.sleep或BlockingQueue.put等 有两种实用策略可用于处理InterruptedException:

  • 传递异常(可能在执行某个特定于任务的清除操作之后) 从而使你的方法也成为可中断的阻塞方法
  • 恢复中断状态 从而使调用栈中的上层代码能够对其进行处理

将InterruptedException传递给调用者

BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException {
		return queue.take();
}

只有实现了线程中断策略的代码才可以屏蔽中断请求 在常规的任务和库代码中都不应该屏蔽中断请求

不可取消的任务在退出前恢复中断

public class NoncancelableTask {
    public Task getNextTask(BlockingQueue<Task> queue) {
        boolean interrupted = false;
        try {
            while (true) {
                try {
                    return queue.take();
                } catch (InterruptedException e) {
                    interrupted = true;
                    // fall through and retry
                }
            }
        } finally {
            if (interrupted)
                Thread.currentThread().interrupt();
        }
    }

    interface Task {
    }
}

示例:计时运行
在外部线程中安排中断(不要这么做)

public class TimedRun1 {
    private static final ScheduledExecutorService cancelExec = Executors.newScheduledThreadPool(1);

    public static void timedRun(Runnable r,
                                long timeout, TimeUnit unit) {
        final Thread taskThread = Thread.currentThread();
        cancelExec.schedule(new Runnable() {
            public void run() {
                taskThread.interrupt();
            }
        }, timeout, unit);
        r.run();
    }
}

这是一种非常简单的方法 但却破坏了以下规则:在中断线程之前 应该了解它的中断策略 由于timeRun可以从任意一个线程中调用 因此它无法知道这个调用线程的中断策略 如果任务在超时之前完成 那么中断timeRun所在线程的取消任务将在timeRun返回到调用者之后启动 我们不知道在这种情况下将运行什么代码 但结果一定是不好的 (可以使用schedule返回的ScheduledFuture来取消这个取消任务以避免这种风险 这种做法虽然可行 但却非常复杂)
而且 如果任务不响应中断 那么timeRun会在任务结束时才返回 此时可能已经超过了指定的时限(或者还没有超过时限) 如果某个限时运行的服务没有在指定的时间内返回 那么将对调用者带来负面影响

在专门的线程中中断任务

public class TimedRun2 {
    private static final ScheduledExecutorService cancelExec = newScheduledThreadPool(1);

    public static void timedRun(final Runnable r,
                                long timeout, TimeUnit unit)
            throws InterruptedException {
        class RethrowableTask implements Runnable {
            private volatile Throwable t;

            public void run() {
                try {
                    r.run();
                } catch (Throwable t) {
                    this.t = t;
                }
            }

            void rethrow() {
                if (t != null)
                    throw launderThrowable(t);
            }
        }

        RethrowableTask task = new RethrowableTask();
        final Thread taskThread = new Thread(task);
        taskThread.start();
        cancelExec.schedule(new Runnable() {
            public void run() {
                taskThread.interrupt();
            }
        }, timeout, unit);
        taskThread.join(unit.toMillis(timeout));
        task.rethrow();
    }
}

在这个示例的代码中解决了前面示例中的问题 但由于它依赖于一个限时的join 因此存在着join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回

通过Future来实现取消
ExecutorService.submit将返回一个Future来描述任务 Future拥有一个cancel方法 该方法带有一个boolean类型的参数mayInterruptIfRunning 表示取消操作是否成功 (这只是表示任务是否能够接收中断 而不是表示任务是否能检测并处理中断) 如果mayInterruptIfRunning为true并且任务当前正在某个线程中运行 那么这个线程能被中断 如果这个参数为false 那么意味着 若任务还没有启动 就不要运行它 这种方式应该用于那些不处理中断的任务中

通过Future来取消任务

public class TimedRun {
    private static final ExecutorService taskExec = Executors.newCachedThreadPool();

    public static void timedRun(Runnable r,
                                long timeout, TimeUnit unit)
            throws InterruptedException {
        Future<?> task = taskExec.submit(r);
        try {
            task.get(timeout, unit);
        } catch (TimeoutException e) {
            // task will be cancelled below
        } catch (ExecutionException e) {
            // exception thrown in task; rethrow
            throw launderThrowable(e.getCause());
        } finally {
            // Harmless if task already completed
            task.cancel(true); // interrupt if running
        }
    }
}

当Future.get抛出InterruptedException或TimeoutException时 如果你知道不再需要结果 那么就可以调用Future.cancel来取消任务

处理不可中断的阻塞
如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞 那么中断请求只能设置线程的中断状态 除此之外没有其他任何作用 对于那些由于执行不可中断操作而被阻塞的线程 可以使用类似于中断的手段来停止这些线程 但这要求我们必须知道线程阻塞的原因

  • Java.io包中的同步Socket I/O
  • Java.io包中的同步 I/O
  • Selector的异步 I/O
  • 获取某个锁

通过改写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();
    }

    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) { /* Allow thread to exit */
        }
    }

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

采用newTaskFor来封装非标准的取消
通过newTaskFor将非标准的取消操作封装在一个任务中

public abstract class SocketUsingTask <T> implements CancellableTask<T> {
    @GuardedBy("this") private Socket socket;

    protected synchronized void setSocket(Socket s) {
        socket = s;
    }

    public synchronized void cancel() {
        try {
            if (socket != null)
                socket.close();
        } catch (IOException ignored) {
        }
    }

    public RunnableFuture<T> newTask() {
        return new FutureTask<T>(this) {
            public boolean cancel(boolean mayInterruptIfRunning) {
                try {
                    SocketUsingTask.this.cancel();
                } finally {
                    return super.cancel(mayInterruptIfRunning);
                }
            }
        };
    }
}


interface CancellableTask <T> extends Callable<T> {
    void cancel();

    RunnableFuture<T> newTask();
}


@ThreadSafe
class CancellingExecutor extends ThreadPoolExecutor {
    public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        if (callable instanceof CancellableTask)
            return ((CancellableTask<T>) callable).newTask();
        else
            return super.newTaskFor(callable);
    }
}

SocketUsingTask实现了CancellableTask 并定义了Future.cancel来关闭套接字和调用super.cancel 如果SocketUsingTask通过其自己的Future来取消 那么底层的套接字将被关闭并且线程将被中断 因此它提高了任务对取消操作的响应性:不仅能够在调用可中断方法的同时确保响应取消操作 而且还能调用可阻塞的套接字I/O方法

停止基于线程的服务
与其他封装对象一样 线程的所有权是不可传递的:应用程序可以拥有服务 服务也可以拥有工作者线程 但应用程序并不能拥有工作者线程 因此应用程序不能直接停止工作者线程 相反 服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线程 这样 当应用程序关闭该服务时 服务就可以关闭所有的线程了 在ExecutorService中提供了shutdown和shutdownNow等方法 同样 在其他拥有线程的服务中也应该提供类似的关闭机制

对于持有线程的服务 只要服务的存在时间大于创建线程的方法的存在时间 那么就应该提供生命周期方法

示例:日志服务

不支持关闭的生产者-消费者日志服务

public class LogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;
    private static final int CAPACITY = 1000;

    public LogWriter(Writer writer) {
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.logger = new LoggerThread(writer);
    }

    public void start() {
        logger.start();
    }

    public void log(String msg) throws InterruptedException {
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        private final PrintWriter writer;

        public LoggerThread(Writer writer) {
            this.writer = new PrintWriter(writer, true); // autoflush
        }

        public void run() {
            try {
                while (true)
                    writer.println(queue.take());
            } catch (InterruptedException ignored) {
            } finally {
                writer.close();
            }
        }
    }
}

为了使像LogWriter这样的服务在软件产品中能发挥实际的作用 还需要实现一种终止日志线程的方法 从而避免使JVM无法正常关闭 要停止日志线程是很容易的 因为它会反复调用take 而take能响应中断 如果将日志线程修改为当捕获到InterruptedException时退出 那么只需中断日志线程就能停止服务
然而 如果只是使日志线程退出 那么还不是一种完备的关闭机制 这种直接关闭的做法会丢失那些正在等待被写入到日志的信息 不仅如此 其他线程将在调用log时被阻塞 因为日志消息队列是满的 因此这些线程将无法解除阻塞状态 当取消一个生产者-消费者操作时 需要同时取消生产者和消费者 在中断日志线程时会处理消费者 但在这个示例中 由于生产者并不是专门的线程 因此要取消它们将非常困难
另一种关闭LogWriter的方法是:设置某个 已请求关闭 标志 以避免进一步提交日志消息

通过一种不可靠的方式为日志服务增加关闭支持

 public void log(String msg) throws InterruptedException {
        if (!shutdownRequested)
        	queue.put(msg);
        else
        	throw new IllegalStateException("logger is shut down");
    }

为LogWriter提供可靠关闭操作的方法是解决竞态条件问题 因而要使日志消息的提交操作成为原子操作 然而 我们不希望在消息加入队列时去持有一个锁 因为put方法本身就可以阻塞 我们采用的方法是:通过原子方式来检查关闭请求 并且有条件地递增一个计数器来 保持 提交消息的权利

向LogWriter添加可靠的取消操作

public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    @GuardedBy("this") private boolean isShutdown;
    @GuardedBy("this") 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) { /* retry */
                    }
                }
            } finally {
                writer.close();
            }
        }
    }
}

关闭ExecutorService

使用ExecutorService的日志服务

public class LogService {
private final ExecutorService exec = newSingleThreadExecutor();
...
public void start() {}

public void stop() throws InterruptedException {
try {
	exec.shutdown();
	exec.awaitTermination(TIMEOUT, UNIT);
} finally {
	writer.close();
}
}
public void log(String msg) {
try {
	exec.execute(new WriteTask(msg));
} catch (RejectedExecutionException ignored) {}
}
}

毒丸 对象
另一种关闭生产者-消费者服务的方式就是使用 毒丸(Poison Pill) 对象:毒丸 是指一个放在队列上的对象 其含义是:当得到这个对象时 立即停止 在FIFO(先进先出)队列中 毒丸 对象将确保消费者在关闭之前首先完成队列中的所有工作 在提交 毒丸 对象之前提交的所有工作都会被处理 而生产者在提交了 毒丸 对象后 将不会再提交任何工作

通过 毒丸 对象来关闭服务

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

只有在生产者和消费者的数量都已知的情况下 才可以使用 毒丸 对象 在IndexingService中采用的解决方案可以扩展到多个生产者:只需每个生产者都向队列中放入一个 毒丸 对象 并且消费者仅当在接收到N(producers)个 毒丸 对象时才停止 这种方法也可以扩展到多个消费者的情况 只需生产者将N(consumers)个 毒丸 对象放入队列 然而 当生产者和消费者的数量较大时 这种方法将变得难以使用 只有在无界队列中 毒丸 对象才能可靠地工作

示例:只执行一次的服务
如果某个方法需要处理一批任务 并且当所有任务都处理完成后才返回 那么可以通过一个私有的Executor来简化服务的生命周期管理 其中该Executor的生命周期是由这个方法来控制的(在这种情况下 invokeAll和invokeAny等方法通常会起较大的作用)

使用私有的Executor 并且该Executor的生命周期受限于方法调用

public class CheckForMail {
    public boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit)
            throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final AtomicBoolean hasNewMail = new AtomicBoolean(false);
        try {
            for (final String host : hosts)
                exec.execute(new Runnable() {
                    public void run() {
                        if (checkMail(host))
                            hasNewMail.set(true);
                    }
                });
        } finally {
            exec.shutdown();
            exec.awaitTermination(timeout, unit);
        }
        return hasNewMail.get();
    }

    private boolean checkMail(String host) {
        // Check for mail
        return false;
    }
}

shutdownNow的局限性
当通过shutdownNow来强行关闭ExecutorService时 它会尝试取消正在执行的任务 并返回所有已提交但尚未开始的任务 从而将这些任务写入日志或者保存起来以便之后进行处理
然而 我们无法通过常规方法来找出哪些任务已经开始但尚未结束 这意味着我们无法在关闭过程中知道正在执行的任务的状态 除非任务本身会执行某种检查 要知道哪些任务还没有完成 你不仅需要知道哪些任务还没有开始 而且还需要知道当Executor关闭时哪些任务正在执行

在ExecutorService中跟踪在关闭之后被取消的任务

public class TrackingExecutor extends AbstractExecutorService {
    private final ExecutorService exec;
    private final Set<Runnable> tasksCancelledAtShutdown =
            Collections.synchronizedSet(new HashSet<Runnable>());

    public TrackingExecutor(ExecutorService exec) {
        this.exec = exec;
    }

    public void shutdown() {
        exec.shutdown();
    }

    public List<Runnable> shutdownNow() {
        return exec.shutdownNow();
    }

    public boolean isShutdown() {
        return exec.isShutdown();
    }

    public boolean isTerminated() {
        return exec.isTerminated();
    }

    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        return exec.awaitTermination(timeout, unit);
    }

    public List<Runnable> getCancelledTasks() {
        if (!exec.isTerminated())
            throw new IllegalStateException(/*...*/);
        return new ArrayList<Runnable>(tasksCancelledAtShutdown);
    }

    public void execute(final Runnable runnable) {
        exec.execute(new Runnable() {
            public void run() {
                try {
                    runnable.run();
                } finally {
                    if (isShutdown()
                            && Thread.currentThread().isInterrupted())
                        tasksCancelledAtShutdown.add(runnable);
                }
            }
        });
    }
}

使用TrackingExecutorService来保存未完成的任务以备后续执行

public abstract class WebCrawler {
    private volatile TrackingExecutor exec;
    @GuardedBy("this") private final Set<URL> urlsToCrawl = new HashSet<URL>();

    private final ConcurrentMap<URL, Boolean> seen = new ConcurrentHashMap<URL, Boolean>();
    private static final long TIMEOUT = 500;
    private static final TimeUnit UNIT = MILLISECONDS;

    public WebCrawler(URL startUrl) {
        urlsToCrawl.add(startUrl);
    }

    public synchronized void start() {
        exec = new TrackingExecutor(Executors.newCachedThreadPool());
        for (URL url : urlsToCrawl) submitCrawlTask(url);
        urlsToCrawl.clear();
    }

    public synchronized void stop() throws InterruptedException {
        try {
            saveUncrawled(exec.shutdownNow());
            if (exec.awaitTermination(TIMEOUT, UNIT))
                saveUncrawled(exec.getCancelledTasks());
        } finally {
            exec = null;
        }
    }

    protected abstract List<URL> processPage(URL url);

    private void saveUncrawled(List<Runnable> uncrawled) {
        for (Runnable task : uncrawled)
            urlsToCrawl.add(((CrawlTask) task).getPage());
    }

    private void submitCrawlTask(URL u) {
        exec.execute(new CrawlTask(u));
    }

    private class CrawlTask implements Runnable {
        private final URL url;

        CrawlTask(URL url) {
            this.url = url;
        }

        private int count = 1;

        boolean alreadyCrawled() {
            return seen.putIfAbsent(url, true) != null;
        }

        void markUncrawled() {
            seen.remove(url);
            System.out.printf("marking %s uncrawled%n", url);
        }

        public void run() {
            for (URL link : processPage(url)) {
                if (Thread.currentThread().isInterrupted())
                    return;
                submitCrawlTask(link);
            }
        }

        public URL getPage() {
            return url;
        }
    }
}

在TrackingExecutor中存在一个不可避免的竞态条件 从而产生 误报 问题:一些被认为已取消的任务实际上已经执行完成 这个问题的原因在于 在任务执行最后一条指令以及线程池将任务记录为 结束 的两个时刻之间 线程池可能被关闭 如果任务是幂等的(Idempotent 即将任务执行两次与执行一次会得到相同的结果) 那么这不会存在问题 在网页爬虫程序中就是这种情况 否则 在应用程序中必须考虑这种风险 并对 误报 问题做好准备

处理非正常的线程终止
当单线程的控制台程序由于发生了一个未捕获的异常而终止时 程序将停止运行 并产生与程序正常输出非常不同的栈追踪信息 这种情况是很容易理解的 然后 如果并发程序中的某个线程发生故障 那么通常并不会如此明显 在控制台中可能会输出栈追踪信息 但没有人会观察控制台 此外 当线程发生故障时 应用程序可能看起来仍然在工作 所以这个失败很可能会被忽略 幸运的是 我们有可以监测并防止在程序中 遗漏 线程的方法

典型的线程池工作者线程结构

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

未捕获异常的处理
上节介绍了一种主动方法来解决未检查异常 在Thread API中同样提供了UncaughtExceptionHandler 它能检测出某个线程由于未捕获的异常而终结的情况 这两种方法是互补的 通过将二者结合在一起 就能有效地防止线程泄漏问题
当一个线程由于未捕获异常而退出时 JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器 如果没有提供任何异常处理器 那么默认的行为是将栈追踪信息输出到System.err

UncaughtExceptionHandler接口

public interface UncaughtExceptionHandler {
	void uncaughtException(Thread t, Throwable e);
}

异常处理器如何处理未捕获异常 取决于对服务质量的需求 最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序日志中 异常处理器还可以采取更直接的响应 例如尝试重新启动线程 关闭应用程序 或者执行其他修复或诊断等操作

将异常写入日志的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);
    }
}

在运行时间较长的应用程序中 通常会为所有线程的未捕获异常指定同一个异常处理器 并且该处理器至少会将异常信息记录到日志中

JVM关闭
JVM既可以正常关闭 也可以强行关闭 正常关闭的触发方式有多种 包括:当最后一个 正常(非守护) 线程结束时 或者当调用了System.exit时 或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-C) 虽然可以通过这些标准方法来正常关闭JVM 但也可以通过调用Runtime.halt或者在操作系统中 杀死 JVM进程(例如发送SIGKILL)来强行关闭JVM

关闭钩子
在正常关闭中 JVM首先调用所有已注册的关闭钩子(Shutdown Hook) 关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程 JVM并不能保证关闭钩子的调用顺序 在关闭应用程序线程时 如果有(守护或非守护)线程仍然在运行 那么这些线程接下来将与关闭进程并发执行 当所有的关闭钩子都执行结束时 如果runFinalizersOnExit为true 那么JVM将运行终结器 然后再停止 JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程 当JVM最终结束时 这些线程将被强行结束 如果关闭钩子或终结器没有执行完成 那么正常关闭进程 挂起 并且 JVM必须被强行关闭 当被强行关闭时 只是关闭JVM 而不会运行关闭钩子

通过注册一个关闭钩子来停止日志服务

public void start() {
	Runtime.getRuntime().addShutdownHook(new Thread() {
		public void run() {
			try { LogService.this.stop(); }
			catch ( InterruptedException ignored ) {}
}
	});
}

守护线程
线程可分为两种:普通线程和守护线程 在JVM启动时创建的所有线程中 除了主线程以外 其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程) 当创建一个新线程时 新线程将继承创建它的线程的守护转态 因此在默认情况下 主线程创建的所有线程都是普通线程
普通线程与守护线程之间的差异仅在于当线程退出时发生的操作 当一个线程退出时 JVM会检查其他正在运行的线程 如果这些线程都是守护线程 那么JVM会正常退出操作 当JVM停止时 所有仍然存在的守护线程都将被抛弃-既不会执行finally代码块 也不会执行回卷栈 而JVM只是直接退出
我们应尽可能少地使用守护线程-很少有操作能够在不进行清理的情况下被安全地抛弃 特别是 如果在守护线程中执行可能包含I/O操作的任务 那么将是一种危险的行为 守护线程最好用于执行 内部 任务 例如周期性地从内存的缓存中移除逾期的数据

此外 守护线程通常不能用来替代应用程序管理程序中各个服务的生命周期

终结器
当不再需要内存资源时 可以通过垃圾回收器来回收它们 但对于其他一些资源 例如文件句柄或套接字句柄 当不再需要它们时 必须显式地交还给操作系统 为了实现这个功能 垃圾回收器对那些定义了finalize方法的对象会进行特殊处理:在回收器释放它们后 调用它们的finalize方法 从而保证一些持久化的资源被释放
由于终结器可以在某个由JVM管理的线程中运行 因此终结器访问的任何状态都可能被多个线程访问 这样就必须对其访问操作进行同步 终结器并不能保证它们将在何时运行甚至是否会运行 并且复杂的终结器通常还会在对象上产生巨大的性能开销 要编写正确的终结器是非常困难的 在大多数情况下 通过使用finally代码块和显式的close方法 能够比使用终结器更好地管理资源 唯一的例外情况在于:当需要管理对象 并且该对象持有的资源是通过本地方法获得的 基于这些原因以及其他一些原因 我们要尽量避免编写或使用包含终结器的类(除非是平台库中的类)

避免使用终结器

小结
在任务 线程 服务以及应用程序等模块中的生命周期结束问题 可能会增加它们在设计和实现时的复杂性 Java并没有提供某种抢占式的机制来取消操作或者终结线程 相反 它提供了一种协作式的中断机制来实现取消操作 但这要依赖于如何构建取消操作的协议 以及能否始终遵循这些协议 通过使用FutureTask和Executor框架 可以帮助我们构建可取消的任务和服务

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值