Java 线程池2

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

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

1.1 CachedThreadPool 线程池

在 java 中定义如下:

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

看下各个参数:

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue 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());
    }

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

1.3 SingleThreadExecutor 线程池

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

单线程池用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,当然正在执行的任务,如果线程中断中未正确处理的话,还是容易丢失任务。

四 诗词欣赏

山有木兮木有枝,心悦君兮君不知。

出自先秦的 [越人歌]

今夕何夕兮,搴舟中流。
今日何日兮,得与王子同舟。
蒙羞被好兮,不訾诟耻。
心几烦而不绝兮,得知王子。
山有木兮木有枝,心悦君兮君不知。

你可能感兴趣的:(Java 线程池2)