《JAVA并发编程实战》取消和关闭

版权声明:本文为博主原创文章,转载请在明显的位置注明出处。如果觉得文章对你有用,给我点个赞呗~ https://blog.csdn.net/sayWhat_sayHello/article/details/83477402

引言

java没有提供任何机制来安全的终止线程。但它提供了一种协作机制:中断。

这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会导致数据结构处于不一致的状态。

任务取消

取消某个操作的原因可能是:

  • 用户请求取消:点击取消按钮等操作
  • 有时间限制的操作
  • 应用程序事件:例如应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索不同区域,当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都要被取消。
  • 错误:爬虫下载资源时,磁盘满,可能所有任务都将被取消
  • 关闭:当一个程序或服务关闭时,必须对正在处理和等待处理的工作执行某种操作。

在Java中没有一种安全的抢占方法停止线程,因此也就没有安全的抢占方法来停止任务。

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

public class PrimeGenerator implements Runnable {
    private final List<BigInteger> primes = new ArrayList<>();
    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);
    }
}


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

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

中断

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

void consumePrimes() throws InterruptedException{
    BlockingQueue<BigInteger> primes = ...;
    BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
    producer.start();
    try{
        while(needMorePrimes()){
            consume(primes.take());
        } 
    } finally {
        producer.cancel();
    }
}

生产者线程生产素数,并将它们放入到一个阻塞队列。如果生产者的速度超过了消费者的处理速度,队列将被填满,put方法也会阻塞。当生产者在put中阻塞时,如果消费者希望取消生产者任务,那么在它调用cancel方法设置cancelled标志时,生产者却永远不能检查这个标志,因为它无法从阻塞的put方法中恢复过来。

每个线程都有一个boolean类型的中断状态。当中断线程时,将这个中断状态设置为true。在Thread中包含了中断线程以及查询线程中断状态的方法。

public class Thread{
    public void interrupt(){}
    public void boolean isInterrupted(){}
    public static boolean interrupted(){}
}

interrupt中断目标线程,isInterrupted方法返回目标线程的中断状态,静态的interrupted方法清除当前线程的中断状态。

阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,且在发现中断时提前返回,他们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。

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

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

对中断操作的正确理解是:它并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。

在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted时返回了true,那么除非要屏蔽这个中断,否则必须对它进行处理——可以抛出InterruptedException,也可以再次调用interrupt来恢复中断状态

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

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

中断策略

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

最合理的中断策略是以某种形式的线程级取消操作或者服务级取消操作:尽快退出,必要时进行清理,通知某个所有者该线程已经退出。此外还可以建立其他的中断策略,例如暂停服务或重新开始服务。

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

任务不会在其自己拥有的线程中执行,而是在某个服务拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池来说,如何在线程池实现以外的代码),应该小心的保存中断状态,这样拥有线程的代码才能对中断做出相应。

线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如关闭方法。

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

响应中断

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

  • 传递异常
  • 恢复中断状态

将InterruptedException传递给调用者:

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

如果不想或者无法传递InterruptedException(或许通过Runnable来定义任务),那么需要寻找另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt来恢复中断状态。

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

对于不支持取消但仍可以调用可中断阻塞方法的操作,他们必须在循环中调用这些方法,并在发现中断后重新尝试。在这种情况下,他们应该在本地保存中断状态,并在返回前回复状态而不是在捕获InterruptedException时恢复状态。如果过早的设置中断状态,就可能引起无限循环,因为大多数可中断的阻塞方法都会在入口处检查中断状态,并且当发现该状态已被设置时会立即抛出InterruptedException(通常,可中断的方法会在阻塞或进行重要的工作前首先检查中断,从而尽快的响应中断)

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

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

如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中断状态来响应中断。

示例:计时运行

private static final ScheduledExecutorService cancelExec = ...;
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();
}

这是一种非常简单的方法,然而却破坏了以下规则:在中断线程之前,应该了解它的中断策略。由于timedRun可以从任意一个线程中调用,因此它无法知道这个调用线程的中断策略。如果任务在超时之前完成,那么中断timedRun所在线程的取消任务将在timedRun返回到调用者后启动。

而且,如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定的时限。

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,表示取消操作是否成功(这只是表示任务是否能够接收中断,而不是表示任务能否检测并处理中断)。如果该参数为true并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没有启动,那就不要启动它”,这种方式应该用于那些不处理中断的任务中。

除非你清楚线程的中断策略,否则不要中断线程。当尝试取消某个任务时,不宜直接中断线程池。

public static void timeRun(Runnable r,long timeout,TimeUnit unit) throws InterruptedException{
    Future<?> task = taskExec.submit(r);
    try{
        task.get(timeout,unit);
    } catch(TimeoutException e){
        //接下来任务将被取消
    } catch(ExecutionException e){
        throw launderThrowable(e.getCauese());
    } finally {
        //如果任务已经结束,那么执行取消操作也不会带来任何影响
        //如果任务正在运行,那么将被打断
        task.cancel(true);
    }
}

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

处理不可中断的阻塞

并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。

由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。

  1. java.io包中的同步Socket I/O。在服务器应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但是通过关闭底层的套接字,可以使得由于执行read或write等方法被阻塞的线程抛出一个SocketException
  2. java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。大多数的Channel都实现了InterruptibleChannel.
  3. Selector的异步I/O。如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  4. 获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断。在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

下面展示的是如何封装非标准的取消操作。

public class ReaderThread extends Thread{
    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[1024];
            while(true){
                int count = in.read(buf);
                if(count < 0) {
                    break;
                } else if(count > 0) {
                    processBuffer(buf,count);
                }
            }
        } catch(IOException e){
            /*允许线程退出*/
        }
    }
}

采用newTaskFor封装非标准的取消

当把一个Callable提交给ExecutorService时,submit方法会返回一个Future,我们可以通过这个Future来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,该接口拓展了Future和Runnable(并由FutureTask实现)。

通过定制表示任务的Future可以改变Future.cancel的行为。例如定制的取消代码可以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。通过改写interrupt方法,ReaderThread可以取消基于套接字的线程。同样,通过改写任务的Future,cancel方法也可以实现类似的功能。

public interface CancellableTask<T> extends Callable<T>{
    void cancel();
    RunnableFuture<T> newTask();
}

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

public abstract class SocketUsingTask<T> implements CancellableTask<T> {
    private Socket socket;
    protected synchronized void setSocket(Socket s){
        socket = s;
    }
    
    public synchronized void cancel(){
        try{
            if(socket!=null){
                socket.close();
            } 
        } catch(IOException ingnored){}
    }
    
    public RunnableFuture<T> newTask(){
        return new FutureTask<T>(this){
            public boolean cancel(boolean mayInterruptIfRunning){
                try{
                    SocketUsingTask.this.cancel();
                } finally {
                    return super.cancel(mayInterruptIfRunning);
                }
            }
        };
    }
}

停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建它们的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控,例如,中断线程或者修改线程优先级等。

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

示例:日志服务

public class LogWriter{
    private final BlockingQueue<String> queue;
    private final LoggerThread thread;
    
    public LogWriter(Writer writer){
        this.queue = new BlokingQueue<>();
        this.logger = new LoggerThread(write);
    }
    
    public void start(){
        logget.start();
    }
    
    public void log(String msg) throws InterruptedException{
        queue.put(msg);
    }
    
    private class LoggerThread extends Thread{
        private final PrintWriter writer;;
        
        public void run(){
            try{
                while(true){
                    writer.println(queue.take());
                }
            } catch(InterruptedException ignored){
                
            } finally {
                writer.close();
            }
            
        }
    }
}

要停止日志线程是很容易的,因为它会反复调用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提供可靠关闭操作的方法是解决竞态条件问题,因而要使日志消息的提交操作成为原子操作。

public class LogService{
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;
    private final PrintWriter writer;
    private boolean isShutdown;
    private int reservations;
    
    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) {
            
        }
    }
}

毒丸对象

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

public class IndexingService{
    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 File root;
    
    public void start(){
        producer.start();
        consumer.start();
    }
    
    public void stop(){
        producer.interrupt();
    }
    
    public void awaitTermination() throws InterruptedException{
        consumer.join();
    }
    
    class CrawlerThread extends Thread{
        public void run(){
            try{
                crawl(root);
            } catch(InterruptedException e){
                
            } finally {
                while(true) {
                    try{
                        queue.put(POISON);
                        break;
                    } catch(InterruptedException e1){
                        /*重新尝试*/
                    }
                }
            }
        }
        
        private void crawl(File root) throws InterruptedException{
            ...
        }
    }
    
    class IndexerThread extends Thread{
        public void run(){
            try{
                while(true){
                    File file = queue.take();
                    if(file == POISON){
                        break;
                    } else {
                        indexFile(file);
                    }
                }
            } catch(InterruptedException consumed){
                
            }
        }
    }
}

示例:只执行一次的服务

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

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

checkMail能在多台主机上并行的检查新邮件。它创建了一个私有的Executor,并向每台主机提交一个任务。然后,当所有邮件检查任务都执行完成后,关闭Executor并等待结束。之所以采用AtomicBoolean是因为能从内部的Runnable中访问hasNewMail标识。

shutdownNow的局限性

当通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在运行的任务并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。

我们无法通过常规方法来找出哪些任务已经开始但尚未结束。这意味着我们无法在关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查。

TrackingExecutor中给出了如何在关闭过程中判断正在执行的任务。

public class TrackingExecutor extends AbstractExecutorService{
    private final ExecutorService exec;
    private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
    
    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);
                    }
                }
            }
        });
    }
    
    //将ExecutorService的其他方法委托给exec
}

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

public abstract class WebCrawler{
    private volatile TrackingExecutor exec;
    private final Set<URL> urlsToCrawl = new HashSet<URL>();
    
    public synchronized void start(){
        exec = new TrackingExecutor(Executors.newCachedThreadPool());
        for(URL url : urlsToCrawl) {
            submitCrawlTask(url);
        }
        urlsToCrawl.clear();
    }
    
    public synchronzied 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 url){
        exec.execute(new CrawlTask(u));
    }
    
    private class CrawlTask implements Runnable{
        private final URL url;
        
        public void run(){
            for(URL link : processPage(url)){
                if(Thread.currentThread().isInterrupted()){
                    return;
                }
                submitCrawlTask(link);
            }
        }
        
        public URL getPage(){
            return url;
        }
    }
    
}

在TrackingExecutor中存在一个不可避免的竞态条件,从而产生“误报”问题:一些被认为已经取消的任务实际上已经执行完成。原因在于,在任务执行最后一条指令以及线程池将任务记录为结束的两个时刻之间,线程池可能被关闭。如果任务是幂等的(Idempotent,即将任务执行两次和执行一次会得到相同的结果),那么不会存在问题。

处理非正常的线程终止

当并发程序中的某个线程发生故障使控制台中可能会输出栈追踪信息,但是没有人会观察控制台。此外,当线程发生故障时,应用程序可能看起来仍然在工作,所以这个失败很可能被忽略。幸运的是,我们有可以监测并防止程序中“遗漏”线程的方法。

导致线程死亡的最主要原因就是RuntimeException。这是unchecked异常,程序默认会在控制台输出栈追踪信息,并终止线程。

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

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

Thread api中同样提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。

当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err.

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

异常处理器如何处理未捕获异常,取决于对服务质量的需求。最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序日志中。

public class USHLogger 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);
    }
}

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

要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally代码来接收通知,因此当线程结束时,将有新的线程来代替它。如果没有提供捕获异常处理器或者其他的故障通知机制,那么任务会悄悄失败,从而导致极大的混乱。如果你希望在任务由于发送异常而失败时获得通知并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。

令人困惑的是,只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,会被封装成ExecutionException抛出。

JVM关闭

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

关闭钩子

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

关闭钩子应该是线程安全的:它们在访问共享数据时必须使用同步机制,并且小心的避免死锁,这和其他并发代码的要求相同。而且,关闭钩子不应该对应用程序的状态或者JVM的关闭原因作出任何假设。最后,关闭钩子应该尽快退出,因为它们会延迟JVM的结束时间,而用户可能希望JVM尽快终止。

关闭钩子可以用于实现服务或应用程序的清理工作,例如清理临时文件。

由于关闭钩子将并发执行,因此在关闭日志文件时可能导致其他需要日志服务的关闭钩子产生问题。实现这种功能的一种方式是对所有服务使用同一个关闭钩子,并且在关闭钩子中执行一系列的关闭操作。

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

守护线程

线程分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程,其他都是守护线程(例如GC等)。

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

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

终结器

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

由于终结器可以在某个由JVM管理的线程中运行因此终结器访问的任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步。在大多数情况下,通过finally代码块和显式的close方法,能够比使用终结器更好的管理资源。唯一的例外是:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。

尽量避免使用终结器

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/83477402