并发编程6:任务或线程的取消与关闭

目录

1、如何取消一个任务

1.1 - 任务的中断机制

1.2 - 中断策略

1.3 - 响应中断

1.4 - 通过 Future 来实现取消

1.5 - 处理不可中断的阻塞

2、停止基于线程的服务(执行器)

2.1 - 使用计数器,记录提交的任务数量

2.2 - 使用毒丸对象

2.3 - 只执行一次的服务

扫描二维码关注公众号,回复: 16423694 查看本文章

3、处理非正常的线程终止

3.1 - 未捕获异常的处理

4、JVM 关闭

4.1 - 关闭钩子

4.2 - 守护线程

4.3 - 终结器


        Java 提供了中断 (Interruption) 协作机制,通过该机制能够使一个线程终止另一个线程的当前工作。

        这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作//中断操作应该由中断线程本身去决定何时中断

1、如何取消一个任务

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

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

        下列程序 PrimeGenerator 将持续地枚举素数,直到它被取消。cancel 方法将设置 cancelled 标志,并且主循环在搜索下一个素数之前会首先检查这个标志。为了使这个过程能可靠地工作,标志 cancelled 必须为 volatile 类型//通过一个标志来进行事件协商

import static java.util.concurrent.TimeUnit.SECONDS;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 一种简单的取消策略
 * 素数生成器
 */
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<>(primes);
    }

    static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        exec.execute(generator);
        try {
            SECONDS.sleep(1);
        } finally {
            //确保该代码必定执行
            generator.cancel();
        }
        List<BigInteger> bigIntegers = generator.get();
        bigIntegers.forEach(System.out::println);
        return bigIntegers;
    }

    public static void main(String[] args) throws InterruptedException {
        aSecondOfPrimes();
    }
}

        一个可取消的任务必须拥有取消策略(Cancellation Policy),在这个策略中将详细地定义取消操作的 "How"、"When" 以及 "What",即其他代码如何请求取消该任务,任务在何时检查是否已经请求了取消,以及在响应取消请求时应该执行哪些操作//如何取消,如何检测取消,如何响应取消

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

1.1 - 任务的中断机制

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

import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;

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 中一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。

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

        Thread 类的 API 文档点击这里

        阻塞库方法,例如 Thread.sleep Object.wait 等,都会检查线程何时中断,并且在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出 InterruptedException 表示阻塞操作由于中断而提前结束。//InterruptedException 异常会清除中断状态

        当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态是否发生了中断。通过这样的方法,中断操作将变得“有黏性”一一如果不触发InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。

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

        对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点)。有些方法,例如 wait、sleep join 等,将严格地处理这种请求,当它们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。

        BrokenPrimeProducer 说明了一些自定义的取消机制无法与可阻塞的库函数实现良好交互的原因。如果任务代码能够响应中断,那么可以使用中断作为取消机制,并且利用许多库类中提供的中断支持//应该优先使用中断作为取消机制

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

        所以,BrokenPrimeProducer 中的问题很容易解决(和简化):使用中断而不是 boolean 标志来请求取消,程序如下所示:

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            //使用中断机制来取消操作
            while (!Thread.currentThread().isInterrupted()) {
                queue.put(p = p.nextProbablePrime());
            }
        } catch (InterruptedException consumed) {
            /*允许线程退出*/
        }
    }

    //取消方法
    public void cancel() {
        interrupt();
    }

1.2 - 中断策略

        中断策略规定线程如何解释某个中断请求一一当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。//怎样响应中断

        区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者,中断线程池中的某个工作者线程,同时意味着“取消当前任务”和“关闭工作者线程”。

        任务不会在其自己拥有的线程中去执行,而是在某个服务拥有的线程中执行。对于非线程所有者的代码来说,应该小心地保存中断状态,这样线程所有者的代码才能对中断做出响应,即使 ''非所有者" 代码也可以做出响应。(当你为一户人家打扫房屋时,即使主人不在,也不应该把在这段时间内收到的杂志扔掉,而应该把杂志收起来,等主人回来以后再交给他们处理,尽管你也可以阅读他们的杂志。)//不要把其他线程的中断状态弄丢(Exception后或许要重新添加中断标记),也不要去响应不是本线程的中断,因为每个线程都应该有自己的中断处理逻辑

        这就是为什么大多数可阻塞的库函数都只是抛出 ImterruptedException 作为中断响应。它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作// 抛出中断异常,给调用者处理

        当任务检查到中断请求时,任务并不需要放弃所有的操作,它可以推迟处理中断请求,并直到某个合适的时刻再进行响应。因此任务需要记住中断请求,并在完成当前任务后抛出 InterruptedException 或者表示已收到中断请求。这项技术能够确保当更新过程中发生中断时,数据结构不会被破坏。//延迟处理中断请求的好处

        无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果将 InterruptedException 传递给调用者外还需要执行其他操作,那么应该在捕获 InterruptedException 之后恢复中断状态//抛出中断异常会清除中断标记

Thread.currentThread().interrupt();

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

        //总之,只有线程自己才知道如何去响应中断请求,如果不清楚一个线程的中断处理逻辑,就不要去中断这个线程,也不要去响应这个线程的中断(而是 catch 异常后,恢复它的中断标记)。

1.3 - 响应中断

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

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

        如果不想传递 InterruptedException,那么就需要使用一种方式来保存中断请求。一种标准的方法就是通过再次调用 interrupt 来恢复中断状态。你不能屏 InterruptedException,例如在 catch 块中捕获到异常却不做任何处理,除非在你的代码中实现了线程的中断策略//总结起来就是这个异常如果不知道怎么处理就传递它,如果知道中断策略就处理它(恢复中断状态也是一种策略),就是不能吞并不处理,这也是所有异常该有的处理方式

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

        //在编程时,一定要注意中断标记的清除点,避免一个线程清除中断标记后,没有正确的响应,从而出现无限循环的情况。多调试程序,相信你不会

        单个任务的线程中断示例:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class InterruptedTest {

    private static ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            try {
                //模拟任务执行时间消耗
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                System.out.println("线程中断执行!任务结束");
                e.printStackTrace();
                return;
            }
            System.out.println("执行任务结束!");
        });
        threadA.start();
        // 使用一个定时任务来中断threadA任务的执行:2s中后中断线程
        scheduledExecutor.schedule(() -> threadA.interrupt(),
            2000, TimeUnit.MILLISECONDS);
        // 使用join等待threadA结束
        threadA.join();
        System.out.println("主线程运行结束");
    }
}

1.4 - 通过 Future 来实现取消

        ExecutorService.submit 将返回一个 Future 来描述任务。Future 拥有一个 cancel 方法,该方法带有一个 boolean 类型的参数 mayInterruptlfRunning,该参数表示如果一个任务正在执行,是否应该进行中断,如果为 false,允许正在执行的任务执行完成。

        通过 Future 来实现任务取消的示例://好处就是,不用管线程是否执行结束,都可以无脑调用 cancel 方法,尽管使用 interrupt() 方法中断一个已经结束的线程也没啥

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class InterruptedTest {

    private static ExecutorService executorService = Executors.newFixedThreadPool(2);

    public static void main(String[] args) throws InterruptedException {
        //1-定义一个带有返回值的任务
        Callable<String> callable = () -> {
            System.out.println("Callable 任务开始执行...");
            Thread.sleep(3000); //模拟执行时间
            System.out.println("Callable 任务执行结束...");
            return "end";
        };
        //2-提交任务
        Future<String> submit = executorService.submit(callable);

        try {
            try {
                //3-获取任务结果
                submit.get(2000, TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                System.out.println("TimeoutException->获取线程返回值超时...");
                e.printStackTrace();
            }
        } catch (ExecutionException e) {
            System.out.println("ExecutionException->任务执行异常");
            e.printStackTrace();
        } finally {
            //如果此时任务已经执行完成,执行取消操作也没有任何的影响
            System.out.println("尝试取消任务的执行");
            //4-取消任务执行-> 参数为false,还是会等待线程任务结束/为true,则直接中断任务
            submit.cancel(false);
        }
        System.out.println("主线程运行结束");
    }
}

        如上边示例,取消那些不再需要结果的任务,是一种良好的编程习惯。

1.5 - 处理不可中断的阻塞

        在Java 库中,许多可阻塞的方法都是通过提前返回或者抛出 InterruptedException 来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻寒方法或者阻塞机制都能响应中断//下面的示例总的来说就是使用关闭机制来进行中断

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

        Java.IO 包中的同步 Socket l/O。在服务器应用程序中,最常见的阻塞 I/O 形式就是对套接字进行读取和写入。虽然 InputStream 和 OutputStream 中的 read 和 write 等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行 read 或 write 等方法而被阻塞的线程抛出一个 SocketException。

        Java.IO 包中的同步 I/O。当中断一个正在 InterruptibleChannel 上等待的线程时,将抛出 ClosedByInterruptException 并关闭链路(这还会使得其他在这条链路上阻塞的线同样抛出 ClosedByInterruptException)。当关闭一个 InterruptibleChannel 时,将导致所有在链路操作上阻塞的线程都抛出 AsynchronousCloseException。大多数标准的 Channel 都实现了 InterruptibleChannel

        Selector 的异步 I/O。如果一个线程在调用 Selector.select 方法(在 java.niochannels 中)时阻塞了,那么调用 close 或 wakeup 方法会使线抛出 ClosedSelectorException 并提前返回。

        获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,在 Lock 类中提供了 lockInterruptibly 方法,该方法允许在等待一个锁的同时仍能响应中断。//体现 Lock 锁灵活的地方

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(); //关闭socket
        } 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) {
    }
}

        上述代码给出了如何封装非标准的取消操作。ReaderThread 管理了一个套接字连接,它采用同步方式从该套接字中读取数据,并将接收到的数据传递给 processBuffer。为了结束某个用户的连接或者关闭服务器,ReaderThread 改写了 interrupt 方法使其既能处理标准的中断,也能关闭底层的套接字。因此,无论 ReaderThread 线程是在 read 方法中阻塞还是在某个可中断的阻塞方法中阻塞,都可以被中断并停止执行当前的工作。//就是针对不能响应中断的阻塞方法,提供了一种看起来可以被中断的方式,考虑得也更加全面,程序不会因为阻塞而迟迟不响应中断请求

2、停止基于线程的服务(执行器)

        服务(执行器)应该提供生命周期方法( Lifecycle Method) 来关闭它自己以及它所拥有的线程。这样,当应用程序关闭该服务(执行器)时,服务(执行器)就可以关闭所有的线程了。在 ExecutorService 中提供了 shutdown 和 shutdownNow 等方法。同样,在其他拥有线程的服务中也应该提供类似的关闭机制。// 服务是指 ExecutorService,线程池的持有对象

2.1 - 使用计数器,记录提交的任务数量

        如果关闭一个服务时,一方面应该禁止新任务的提交,另一方面,该服务尚有未执行完成的任务,应该等待所有任务执行完成,才能彻底关闭服务。

        在大多数服务器应用程序中都会用到日志,例如,在代码中插入 println 语句就是一种简单的日志。像 PrintWriter 这样的字符流类是线程安全的,因此这种简单的方法不需要显式的同步。然而,这种内联日志功能会给一些高容量的(Highvolume)应用程序带来一定的性能开销。另外一种替代方法是通过调用 log 方法将日志消息放入某个队列中,并由其他线程来处理//内联日志效率低,影响程序性能

        下边是一个 log 日志的实现程序,该程序启动一个新的线程来打印日志,同时可以安全的关闭 LogService 日志服务:

public class LogService {
    //任务队列
    private final BlockingQueue<String> queue;
    private final LoggerThread          loggerThread;
    private final PrintWriter           writer;
    //关闭标志:因为该变量一直在锁中操作,所以不需要加 volatile
    private       boolean               isShutdown;
    //提交任务计数
    private       int                   reservations;

    public LogService(Writer writer) {
        this.queue        = new LinkedBlockingQueue<>();
        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 提供了两种关闭方法:使用 shutdown 正常关闭以及使用 shutdownNow 强行关闭。在进行强行关闭时,shutdownNow 首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单。

        这两种关闭方式的差别在于各自的安全性响应性:强行关闭的速度更快,但风险也更大,因为任务很可能在执行到一半时被结束,而正常关闭虽然速度慢,但却更安全,因为ExecutorService 会一直等到队列中的所有任务都执行完成后才关闭。在其他拥有线程的服务中也应该考虑提供类似的关闭方式以供选择。//两中关闭方式,一种快速响应,但是不安全,一种安全,但不能快速响应

        所以,上边 logservice 中的 stop 方法也可以这样写://注意,不要中断线程,否则不会等待所有任务执行完成,等待过程可以被中断

    //使用线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();

    public void stop() {
        try {
            //关闭executorService,等待所有任务执行完成
            executorService.shutdown();
            //阻塞线程,直到所有任务完成
            executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }

2.2 - 使用毒丸对象

        另一种关闭生产者 - 消费者服务的方式就是使用“毒丸(Poison Pil)”对象:“毒丸”是指一个放在队列上的对象,其含义是:“当得到这个对象时,立即停止。”在 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<>(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();
    }
}

        只有在生产者和消费者的数量都已知的情况下,才可以使用“毒丸”对象。当生产者和消费者的数量较大时,这种方法将变得难以使用。只有在无界队列中,“毒丸”对象才能可靠地工作//如果有5个消费者,那么就放入5个毒丸对象,中断5个消费者

2.3 - 只执行一次的服务

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

3、处理非正常的线程终止

        任何代码都可能抛出一个 RuntimeException。每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目地认为它一定会正常返回,或者一定会抛出在方法原型中声明的某个已检查异常。对调用的代码越不熟悉,就越应该对其代码行为保持怀疑。

        由于遗漏线程而造成的严重后果的例子:Timer 表示的服务将永远无法使用。//如果执行定时任务的线程抛出了异常,该线程会终结,那么其他任务将再也得不到执行

        所以,当通过 Runnable 这样的抽象机制来调用未知的和不可信的代码时。需要考虑捕获 RuntimeException,线程应该在 try-catch 代码块中调用这些任务。//捕获异常,防止线程遗漏/终结

        线程遗漏:当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。

3.1 - 未捕获异常的处理

        除了使用主动方法来解决未检查异常。在 Thread API中同样提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。这两种方法是互补的,通过将二者结合在一起,就能有效地防止线程泄漏问题。//主动检查 + 遗漏捕获

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

import java.util.logging.Level;
import java.util.logging.Logger;

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

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

4、JVM 关闭

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

4.1 - 关闭钩子

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

4.2 - 守护线程

        如果希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍 JVM 的关闭。在这种情况下就需要使用守护线程(Daemon Thread)。//典型的就是垃圾回收器

        线程可分为两种:普通线程和守护线程。在 JVM 启动时创建的所有线中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。在默认情况下,主线程创建的所有线程都是普通线程

        普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时 JVM 会检查其他正在运行的线程,如果这些线程都是守护线程,那么 JVM 会执行正常退出操作。当 JVM 停止时,所有仍然存在的守护线程都将被抛弃,既不会执行 finally 代码块,也不会执行回卷栈。//守护线程和普通线程的操作仅在于退出时的待遇

        我们应尽可能少地使用守护线程一一很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含 I/O 操作的任务,那么将是一种危险的行为。守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。

4.3 - 终结器

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

        由于终结器可以在某个由 JVM 管理的线程中运行,因此终结器访问的任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步。终结器并不能保证它们将在何时运行甚至是否会运行,并且复杂的终结器通常还会在对象上产生巨大的性能开销。要编写正确的终结器是非常困难的。

        在大多数情况下,通过使用 finally 代码块和显式的 close 方法,能够比使用终结器更好地管理资源。唯一的例外情况在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。基于这些原因以及其他一些原因,我们要尽量避免编写或使用包含终结器的类(除非是平台库中的类)。//终结器只应该给调用本地方法的类使用

        避免使用终结器

        至此,全文到此结束。

猜你喜欢

转载自blog.csdn.net/swadian2008/article/details/119601492