美文网首页
java线程池二

java线程池二

作者: 明翼 | 来源:发表于2020-07-23 21:08 被阅读0次

    一 最好不要用Java中默认的线程池

    好像面试中常考这种题目,简单一句话就是因为容易OOM或因无法创建线程报错。

    1.1 CachedThreadPool线程池

    在java中定义如下:

    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

    看下各个参数:

       public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }
    

    阻塞队列用的是SynchronousQueue队列,这是无存储队列,也就是如果没有线程处理它,那就一直处于阻塞状态。根据上次的图,队列处于满的,那就会创建非核心线程,所以newCachedThreadPool 来个任务,如果没有空闲线程,就创建一个线程。这个线程池的最大线程数:maximumPoolSize 定义为Integer.MAX_VALUE所以几乎是无限的,如果任务处理的比较慢,就会创建越来越多的线程,最后导致无法创建线程或oom挂掉。

    1.2 FixedThreadPool 线程池

    定义如下:

      public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    固定线程数的线程池,核心线程数和最大线程数的数量是一样的,这个没问题,用的阻塞队列是:
    LinkedBlockingQueue 这个队列有个特点,就是无界,可以无限存放任务。如果任务执行的速度比较慢,那就造成在队列中的任务越来越多,最终造成OOM。

    1.3 SingleThreadExecutor 线程池

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    

    单线程池用FinalizableDelegatedExecutorService 包装了下,如下:

        static class FinalizableDelegatedExecutorService
            extends DelegatedExecutorService {
            FinalizableDelegatedExecutorService(ExecutorService executor) {
                super(executor);
            }
            protected void finalize() {
                super.shutdown();
            }
        }
    

    FinalizableDelegatedExecutorService 实现了finalize ,在jvm进行垃圾回收的时候,会调用finalize方法,也就是说我们可以不关闭,虚拟机也会关闭线程池,不过由于这个方法不是一定会调用,为安全期间,还是要主动调用shutdown等关闭方法来关闭线程池。
    继承的DelegatedExecutorService 对一些线程池的方法进行了一层包装,限制了调用了线程池的方法。不具备ThreadPoolExecutor线程池的所有功能。

    1.4 ScheduledThreadPool 线程池

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

    用到的队列为DelayedWorkQueue这种队列有利于延迟执行任务,我们调度任务的时候,可能5s后执行任务,也可能1个小时后执行任务,这样将5s后执行的任务放在队列的前面,这样获取任务比较容易。
    队列采用堆来实现,这让我想起来大顶堆或小顶堆,用来存放超时或排序的数据挺好的。

    这个线程池的问题在于最大线程数:Integer.MAX_VALUE 为无限的,这样如果任务执行的速度比较慢,线程池创建会越来越多,会导致线程创建失败或OOM的问题。

    newSingleThreadScheduledExecutor 也是一样的问题。

    二 自己如何定义线程池

    前面说了,java的默认线程池有一些问题,不建议使用,那么就需要自己设置线程池。我们可以利用ThreadPoolExecutor来定制属于自己的线程池。

    那么最重要的就是两点:1. 我们需要多少个线程; 2. 我们用什么阻塞队列比较好。

    线程池的线程数量
    线程池的线程不能太少,如果太少,就难以充分利用cpu来提升性能; 线程池的线程也不能太多,如果远大于cpu的核心数量,那么会有很多线程交叉执行,cpu需要进行大量的线程切换,这样反而会影响程序的性能。

    对于任务来说,如果是cpu密集型的任务,如果此主机主要是运行这个程序,可以将线程数设置为主机cpu核心数; 如果也有其他任务在运行,想性能高点,可以适当扩大些,不建议超过2倍的核心数。

    对于IO密集型任务,线程多数是被IO阻塞了,所以可以有更多的线程数,因为如果线程少,这些线程都因为IO阻塞的话,那么cpu的性能就没有充分利用。
    有个计算公式:

    线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
    

    阻塞队列
    除了刚才的线程池,最重要的参数就是阻塞队列了,上文中的几种Java线程池默认的队列,都不是太合适,还有一种队列ArrayBlockingQueue ,这个队列采用数组来实现,容量是固定的,如果任务过多,队列会满,线程池的线程数量没有达到最大线程的时候,会继续创建线程;如果线程池的线程数量达到了最大数量,后续的任务会被拒绝,拒绝策略按照前一篇文章,也有可能会丢失,不过这个比用无限队列引起OOM要好。

    定制线程池的时候,还有创建线程工厂,这个我们可以自己定义一个,比如可以设置创建线程的名字;

    拒绝策略
    拒绝可以参考上一篇文章,有很多种拒绝策略,如果上述的策略不够,我们也可以自己写一个,是将任务持久化,还是直接报错,还是简单的日志记录下,都可以自己封装。

    public interface RejectedExecutionHandler {
        void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }
    

    自己顶一个拒绝策略类实现这个接口即可。

    三 线程池的关闭

    我们停止线程池,通常的做法是先调用shutdown,然后调用awaitTermination等待几秒之后,如果还没有停止则直接调用shutdownNow方法进行线程池直接关闭。

    这种方法现在看来还是不错的,shutdown调用了,不是直接停止线程池,是需要等待线程池将正在执行的任务和队列中的任务执行完成。这样比较安全,调用shutdown之后,后续的任务就不再接收了。但是如果我们前台停止的时候,如果队列中一直有任务,那么停止一直停不掉体验不好。

    所以调用之后再次调用awaitTermination代码如下:

            ExecutorService fix = Executors.newFixedThreadPool(10);
            try {
                fix.awaitTermination(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    

    awaitTermination 这个行数三种情况下会返回:

    1. 线程池真正停止了,返回true。
    2. 线程池超过等待时间后,如果还没停止,返回false。
    3. 等待被中断。

    shutdownNow 这个比较狠,像kill -9 ,但是java中也不是一定会杀掉线程,因为shutdownNow 会对线程池中的线程产生中断,如果正在执行的线程不响应中断,忽略中断,那么还会继续停止。另外,此行数是有返回值的,将线程池中队列中未执行的任务汇总到一个list中返回,这样可以防止任务的丢失:List<Runnable>,当然正在执行的任务,如果线程中断中未正确处理的话,还是容易丢失任务。

    相关文章

      网友评论

          本文标题:java线程池二

          本文链接:https://www.haomeiwen.com/subject/fhtklktx.html