线程池的原理和使用(三)

shutdown()方法原理:

 public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //检查权限.
            checkShutdownAccess();
            //设置线程池状态
            advanceRunState(SHUTDOWN);
            //中断空闲线程.
            interruptIdleWorkers();
            //钩子函数,主要用于清理一些资源.
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

shutdown方法首先加锁,其次检查调用者是否具有执行线程关闭的Java Security权限.接着shutdown方法会将线程状态变为SHUTDOWN,在这之后线程池不在接受提交的新任务.如果还在提交新的任务,就会使用线程池的拒绝策略.

 shutdownNOW()方法原理:

public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //检查状态.
            checkShutdownAccess();
            //将线程池状态改为STOP.
            advanceRunState(STOP);
            //中断所有线程,包括工作线程以及空闲线程.
            interruptWorkers();
            //丢弃工作队列中剩余的任务.
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

shutdownNow方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及空闲线程),最后清空工作队列,取出工作队列中未完成的任务返给调用者.与有序的shutdown方法相比,shutdownNow方法比较粗暴,直接中断线程.不过需要注意的是,中断线程并不是直接结束,而是设立了中断状态.

awaitTermination方法:

调用了线程池的shutdown和shutdownNow方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等到线程池关闭完成,需要调用awaitTermination方法进行主动等待.

threadPool.shutdown();
        try {
            while (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.out.println("线程池任务还未执行完毕");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

如果线程池完成关闭 awaitTermination方法会返回true,否则等待时间超过指定时间会返回false.如果需要调用waitTermination方法建议不是永久等待,而是设置重试次数.

优雅的关闭线程池:

大家可以结合上面的几种方式优雅的关闭一个线程池.

1:执行shutdown方法,拒绝提交新的任务,并等待所有的任务有序的执行完成.

2:执行awaitTermination(long timeout, TimeUnit unit)方法,指定超时时间,判断是否关闭所有任务,线程池关闭完成.

3:如果awaitTermination方法返回false,或者被中断,就调用shutdownNow方法立即关闭线程池所有任务.

4:补充执行awaitTermination(long timeout, TimeUnit unit),判断线程池是否关闭完成.如果超时,就进行循环关闭,循环一定的次数,如1000次.不管关闭线程池,直到其关闭或者循环结束.

public class ThreadUtil {

    public static void shutdownThreadPoolGracefully
         (ExecutorService threadPool) {
        //如果线程以及关闭就返回.
        if (!((threadPool instanceof ExecutorService) 
        || threadPool.isTerminated())) {
            return;
        }

        //拒绝接收任务.
        try {
            threadPool.shutdown();
        } catch (SecurityException e) {
            return;
        } catch (NullPointerException e) {
            return;
        }

        try {
            //等待60秒,等待线程池中的任务完成.
            if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                //调用shutdownNow立即关闭,
                threadPool.shutdownNow();
                //在等待60秒,如果还未结束,可以再次尝试或者直接放弃.
                if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                    System.out.println("线程池任务未正常执行结束");
                }
            }
        } catch (InterruptedException e) {
            //捕获异常,重新调用shutdownNow方法.
            threadPool.shutdownNow();
        }

        //仍然没有关闭,循环关闭1000次,每次等待10毫秒.
        if (!threadPool.isTerminated()) {
            try {
                for (int i = 0; i < 1000; i++) {
                    if (threadPool.awaitTermination
                     (10, TimeUnit.MILLISECONDS)) {
                        break;
                    }
                }
            }catch (InterruptedException e){
                System.err.println(e.getMessage());
            }catch (Throwable e){
                System.err.println(e.getMessage());
            }
        }
    }
}

 优雅关闭的原理和线程状态有关系,包括线程的中断点的完美结合.

注册JVM钩子函数自动关闭线程池:

如果使用了线程池,可以再JVM注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动将线程池优雅关闭,以确保资源正常释放.

public class ThreadUtil {

    //懒汉式单例加载线程池.
    static class ThreadPoolLazyHolder{
        //线程池
        static final ScheduledThreadPoolExecutor EXECUTOR =
                new ScheduledThreadPoolExecutor(1);
     
        static {
            //注册JVM关闭时的钩子函数.
            Runtime.getRuntime().addShutdownHook(new Thread(()->{
                shutdownThreadPoolGracefully(EXECUTOR);
            }));
        }
    }
}

这个例子和上面那个util例子是一起的.就不重复了.

Executors快捷创建线程的潜在问题:

1:Executors.newFixedThreadPool固定线程池的潜在问题
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(
                                      //核心线程数.
                                      nThreads,
                                      //最大线程数 
                                      nThreads,
                                      //线程最大空闲时间.
                                      0L,
                                      //时间单位.毫秒. 
                                      TimeUnit.MILLISECONDS,
                                      //任务的排队队列,无界队列.
                                      new LinkedBlockingQueue<Runnable>());
    }

 可以看到阻塞队列没有指定大小.看源码

 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

参数大小默认为 Integer.MAX_VALUE.如果任务提交速率持续大于处理速度,就会造成大量任务等待,如果队列很大,很可能会导致JVM出现OOM异常,内存资源耗尽.

2:Executors.newSingleThreadExecutor单例线程池的潜在问题:
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1,
                                    1,
                                    0L, 
                                    TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

 以上代码首先通过调用工厂方法FinalizableDelegatedExecutorService对该固定线程池进行包装,这一层包装的作用是防止线程池的核心线程被动态的修改.

 可以看到阻塞队列没有指定大小.看源码

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

 阻塞队列的默认构造方法也是一个无界队列.

3:Executors.newCachedThreadPool缓存线程池潜在问题:
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 
                                      Integer.MAX_VALUE,
                                      60L, 
                                      TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());

通过源码可以看到创建了一个核心线程数为0 最大线程数不设限制的线程池,所以理论上可缓存线程池可以拥有无数个工作线程.

可缓存线程池的工作队列为SynchronousQueue同步队列,这个队列类似于一个接力棒,与出队必须同时传递.当可缓存线程池有任务到来时,新任务会被插入到SynchronousQueue实例中,由于SynchronousQueue是同步队列,会从线程池中寻找可用的线程来执行,没有的话就创建一个新的线程.

SynchronousQueue是一个比较特殊的阻塞队列实现类.没有容量,每一个插入操作都对应着删除操作,反之每个删除操作都对应着插入操作.也就是说,提交的任务不会被保存,而是将新任务交给空闲线程执行,如果没有空闲线程就创建一个新的线程,如果线程数已经大于最大线程数,就执行拒绝策略.使用这种阻塞队列就需要把最大线程数设置的很大.而这个线程池的最大问题就在最大线程数量不设上限.如果任务提交过多会造成OOM异常,甚至因为线程过多,造成资源耗尽.

4:Executors.newScheduledThreadPool可调度线程池潜在问题
 public static ScheduledExecutorService 
    newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

Executors.newScheduledThreadPool工厂方法调用了ScheduledThreadPoolExecutor实现类的构造器,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor的普通线程池类,在构造器内部进一步调用了父类的构造器.

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

以上代码创建了一个 ThreadPoolExecutor实例,其中corePoolSize为传递来的参数.最大线程数为Integer.MAX_VALUE,表示线程数不设上限,阻塞队列为DelayedWorkQueue实例,这是一个按照到期时间升序的阻塞队列.

可调度线程池的潜在问题是最大线程数不设置上限,如果过期任务过多的话,会导致CPU的线程资源耗尽,可以通过源码看到潜在问题首先还是在阻塞队列为无界队列,可能会堆积大量的任务,造成OOM异常.

确定线程池的线程数

按照任务类型对线程池进行分类

1:IO密集型任务

此类任务主要执行IO操作.由于IO操作的时间比较长,导致CPU的使用率不高,这类任务CPU常处于空闲状态.

2:CPU密集型任务

此类任务主要是执行计算任务,由于响应时间很快,CPU一直运行,这种任务的CPU利用率很高.

3:混合型任务

此类任务是既要执行逻辑计算,又要进行IO操作(如RPC调用 数据库访问).相对来说,由于执行IO操作的耗时比较长(一次网络往返往往在数百毫秒级别),这种任务的CPU利用率也不是很高.Web服务器的HTTP请求处理操作为此类任务的典型例子.

IO密集型任务确定线程数

由于IO密集型任务CPU使用率比较低.导致线程空余时间很多.因此通常需要开CPU核数两倍的线程.当IO线程空闲时,可以使用其他线程继续使用CPU.提高CPU的使用率.

CPU密集型任务确定线程数

 CPU密集型任务也叫计算密集型任务,其特点是要进行大量的计算需要消耗CPU资源.比如计算圆周率 对视频进行高清解码.CPU密集型任务虽然可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,,所以为了高效的利用CPU,线程数量应当等于CPU的核心数就行.

混合型任务确定线程数

混合型任务既要执行逻辑计算,又要进行IO操作(如RPC调用 数据库访问).所以混合型任务CPU利用率不是很高,但是非CPU耗时往往又是CPU耗时的数倍.比如WEB应用处理HTTP请求时,一次请求处理会包括DB操作 RPC操作 缓存操作等多种耗时操作.

业界有一种比较成熟的估算方式来确定混合任务的线程数.

最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数

经过换算,上面的公式可以转换为.

最佳线程数=(线程等待时间与线程CPU时间之比+1)*CPU核数.

通过公式可以看出来,线程等待时间越长,线程数越多.CPU耗时所占比例越少,线程数越少.

不管怎么做可能都是不完美的,坚持或者不坚持.但是我觉着有一种大爱叫做,我在.

我在坚持,我在坚持最需要的时候会义无反顾的站出来.

如果大家喜欢我的分享的话.可以关注一下微信公众号.

心有九月星辰

 

 

 

 

 
 

 

                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值