线程的取消与关闭

线程的取消与关闭

1. 处理不可中断的阻塞

在Java库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来相应中断请求的,然而并非所有的可阻塞方法或者阻塞机制都能相应中断。如果一个线程由于可执行同步的Socket IO或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。
1. Socket I/O
在服务应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入,虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或者write等方法而阻塞的线程抛出一个SocketException
2. Selector 异步I/O
如果一个线程在调用Selector.select方法时阻塞了,那么调用close或者wakeup方法会使线程抛出CloseSelectorException并提前返回
3. 获取某个锁
如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断。因为线程认为它肯定能获得锁,所以将不会理会中断请求。但是在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

2. 采用newTaskFor来封装非标准的取消

当把一个Callable提交给ExecutorService时,submit方法会返回一个Future,我们可以通过这个Future来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,该接口扩展了Future和Runnable。
通过定制表示任务的Future可以改变Future.camcel行为,例如,定制的取消代码可以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作等等。

3. shutdownNow的局限性

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

import java.util.*;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 定制的执行池,执行shutdownNow强制关闭线程池时,
 * 会返回未执行和执行中的线程
 */
public class TrackingExecutor extends AbstractExecutorService {

    private final ExecutorService exec;

    private final Set<Runnable> tasksCancelledAtShutdown =
            Collections.synchronizedSet(new HashSet<Runnable>());

    public TrackingExecutor() {
        exec = Executors.newCachedThreadPool();
    }

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

    /**
     * 定制execute,返回中断以及未执行的Runnable
     * @param command
     */
    public void execute(final Runnable command) {
        exec.execute(new Runnable() {
            public void run() {
                try {
                    command.run();
                } finally {
                    if (isShutdown() && Thread.currentThread().isInterrupted()) {
                        tasksCancelledAtShutdown.add(command);
                    }
                }
            }
        });

    }

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

4.JVM关闭

4.1 关闭钩子

在正常关闭中,JVM首先调用所有已注册的关闭钩子,关闭钩子是通过Runtime.addShutdownHook注册的但尚未开始的线程,JVM并不能保证关闭钩子的调用顺序,在关闭应用程序线程时,如果有守护或者非守护线程仍然在运行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起并且JVM必须被强行关闭,将被强行关闭时,只会关闭JVM而不会关闭钩子。
关闭钩子可以用于实现服务或者应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。

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

            }
        }
    });
}

4.2 守护线程

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

4.3 终结器

当不在需要内存资源时,可以通过垃圾回收器来回收他们,但是对于一些其他资源,例如文件句柄或套接字,当不在需要时,必须显式的交还给操作系统。为了实现这个功能,GC对那些定义了finalize方法的对象会进行特殊处理:在回收器释放他们后,调用他们的finalize方法,从而保证一些持久化的资源被释放。
避免使用终结器

猜你喜欢

转载自blog.csdn.net/rambokitty/article/details/80492690