Java线程池那些事儿

阿里巴巴Java手册中,关于线程池:

  • 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
  • 使用线程池的好处,是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
  • 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

线程池的好处:

  • 可以重用线程,避免线程创建的开销;
  • 任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。

一、JUC线程池详解

Java JUC包中的实现类是ThreadPoolExecutor,继承AbstractExecutorService,实现了ExecutorService。

ThreadPoolExecutor比较重要的两个构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

其中:

  • corePoolSize : 核心线程数量
  • maximumPoolSize : 最大线程数量
  • keepAliveTime / unit : 空闲线程存活时间
  • workQueue : 任务队列
  • threadFactory : 线程工厂
  • handler : 拒绝策略

1、corePoolSize,核心线程数量,刚创建一个线程池后,不会创建任何线程。

当有新任务到来时,如果当前线程数量小于corePoolSize,会创建一个新线程执行该任务,即使其他线程是空闲的,也会创建新线程。如果线程数量大于corePoolSize,不会立即创建新线程,而是尝试排队,如果因为队列满了或其他原因不能立即入队,就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有达到,继续创建新线程,直到线程数达到maximumPoolSize。

流程图

核心线程:当线程个数小于corePoolSize时的线程。

核心线程默认行为:

  • 不会预先创建,只有当有任务时才会创建。
  • 不会因为空闲而被终止,keepAliveTime参数不适用核心线程。

改变这些默认行为ThreadPoolExecutor有如下方法:

// 预先创建所有核心线程
public int prestartAllCoreThreads();
// 创建一个核心线程,如果所有核心线程都已经创建,返回false
public boolean prestartCoreThread();
// 参数为true时,允许keepAliveTime适用于核心线程
public void allowCoreThreadTimeOut(boolean value)

2、keepAliveTime,目的是为了释放多余的线程资源。当线程池中线程个数大于corePoolSize时额外空闲线程的存活时间。一个非核心线程,在空闲等待新任务的最长等待时间。0表示所有线程都不会超时终止。

3、workQueue,阻塞队列BlockingQueue:

  • LinkedBlockingQueue:基于链表,可以指定最大长度,默认无界
  • ArrayBlockingQueue:基于数组,有界
  • PriorityBlockingQueue:基于堆,无界阻塞优先级队列
  • SynchronousQueue:没有实际存储空间的同步阻塞队列。

对于无界队列,线程个数最多只能达到corePoolSize,达到corePoolSize后,新任务总会排队,maximumPoolSize就没有意义了。

对于SynchronousQueue,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到maximumPoolSize。

4、handler,RejectedExecutionHandler,任务拒绝策略

队列有限,并且maximumPoolSize有限,当队列排满,线程个数也达到maximumPoolSize,此时新任务会触发线程池的任务拒绝策略。

ThreadPoolExecutor实现了4种处理方式:

  • ThreadPoolExecutor.AbortPolicy:默认方式,抛出异常
  • ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行
  • ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后新任务入队
  • ThreadPoolExecutor.CallerRunPolicy:在任务提交者线程中执行新任务,而不是交给线程池的线程执行。

拒绝策略只有在队列有界,maximumPoolSize有限的情况下才会触发。

如果队列无界,服务不了的任务总是会排队;请求队列可能会消耗非常大的内存,甚至引发OOM;

如果队列有界但maximumPoolSize无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完成。

在任务量非常大的场景中,需要让拒绝策略有机会执行。

5、threadFactory,线程工厂

ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory。

创建一个线程,设置默认名称(pool-线程池编号-thread-线程编号),设置daemon属性为false,设置线程优先级为标准默认优先级(5)。

如果要自定义线程属性,可以实现自定义的ThreadFactory。

二、工厂类Executors

虽然不推荐直接使用Executors工厂类创建线程池,但还是要了解一下利弊。

1.newSingleThreadExecutor

只使用一个线程,使用无界队列,线程创建后不会超时,顺序执行所有任务。

适用于需要保证所有任务被顺序执行的场合。

无界队列,如果排队任务过多,可能会消耗过多的内存。

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

2.newFixedThreadPool

使用固定数目的线程,使用无界队列,线程创建后不会超时终止。

无界队列,如果排队任务过多,可能会消耗过多的内存。

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

3.newCachedThreadPool

核心线程数为0,最大线程数为Integer的最大值,线程空闲时间为60秒,队列为SynchronousQueue。

当新任务提交,正好有空闲线程在等待任务,则空闲线程接受该任务,否则总是创建新线程。对任一空闲线程60s内没有新任务则终止。

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

使用场景对比:

  • 系统负载很高 - newFixedThreadPool
    • newFixedThreadPool,通过队列对新任务排队,保证有足够的资源处理实际任务;
    • newCachedThreadPool,为每个任务创建一个线程,导致创建过多的线程,竞争CPU和内存资源;
  • 系统负载不太高,单个任务执行时间比较短 - newCachedThreadPool
    • newCachedThreadPool,效率可能更高,因为任务可以不经排队,直接交给一个空闲线程或新建线程。
  • 系统负载可能极高 - 两者都不是最好的选择,应根据具体情况自定义合适的参数。
    • newFixedThreadPool,队列过长
    • newCachedThreadPool,线程过多
  • CPU密集型任务(计算型任务),一般线程数量为CPU数量的1~2倍,过多线程可能增大上下文切换的开销。
  • IO密集型任务,相对比CPU密集型任务,需要多一些线程,根据具体的IO阻塞时长进行考量决定。如tomcat,默认最大线程数为200。

网上的帖子质量参差不齐,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

猜你喜欢

转载自blog.csdn.net/hellboy0621/article/details/105962987