前言

线程池内部是多个线程的集合,在创建初期,线程池会创建出多个空闲的线程,当有一个任务需要执行时,线程池会选择出一个线程去执行它,执行结束后,该线程不会被销毁,而是可以继续复用。

使用线程池可以大大减少线程频繁创建与销毁的开销,降低了系统资源的消耗。当任务来临时,直接复用之前的线程,而不是先创建,提高了系统的响应速度。此外,线程池可以控制最大的并发数,避免资源的过度消耗。


简单实例

先给出一个线程池的简单例子:

package com.xue.testThreadPool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(2); for (int i = 0; i < 4; i++) { int finalI = i;
            threadPool.execute(new Runnable() { @Override public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在执行任务" + finalI);
                }
            });
        }
        threadPool.shutdown();
    }
}

输出如下:

可见,2个线程总共执行了4个任务,线程得到了复用。


线程池的核心参数

这些核心参数位于ThreadPoolExecutor的构造方法中:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize               核心线程数,或者说常驻线程数,线程池中最少线程数
  • maximumPoolSize      最大线程数
  • keepAliveTime             空闲线程的存活时间,线程池中当前线程数大于corePoolSize时,那些空闲时间达到keepAliveTime的空闲线程,它们将会被销毁掉
  • TimeUnit                       keepAliveTime的时间单位
  • workQueue                   任务队列,存放未被执行的任务
  • threadFactory               创建线程的工厂
  • handler                          拒绝策略,当前线程数≥最大线程数且任务队列满的时候,对后续任务的拒绝方式

线程池的种类

不同的线程池有不同的适用场景,本质上都是在Executors类中实例化一个ThreadPoolExecutor对象,只是传入的参数不一样罢了。

线程池的种类有以下几种:

newFixedThreadPool

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

创建一个固定大小的线程池,即核心线程数等于最大线程数,每个线程的存活时间和线程池的寿命一致,线程池满负荷运作时,多余的任务会加入到无界的阻塞队列中,newFixedThreadPool可以很好的控制线程的并发量。

newCachedThreadPool

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

创建一个可以无限扩大的线程池,当任务来临时,有空闲线程就去执行,否则立即创建一个线程。当线程的空闲时间超过1分钟时,销毁该线程。适用于执行任务较少且需要快速执行的场景,即短期异步任务。

newSingleThreadExecutor

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

创建一个大小为1的线程池,用于顺序执行任务。

newScheduledThreadPool

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

创建一个初始大小为corePoolSize的线程池,线程池的存活时间没有限制,newScheduledThreadPool中的schedule方法用于延时执行任务,scheduleAtFixedRate用于周期性地执行任务。


 线程池执行任务的流程

  • 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  • 当线程池中线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 。

  • 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务。

  • 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理。

  • 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。

  • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

使用更加直观的流程图来描述:

注:此章节参考通俗易懂,各常用线程池执行的-流程图


工作队列

工作队列用来存储提交的任务,工作队列一般使用的都是阻塞队列。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

阻塞队列一般由以下几种:

LinkedBlockingQueue  

由单链表实现的无界阻塞队列,遵循FIFO。注意这里的无界是因为其记录队列大小的数据类型是int,那么队列长度的最大值就是恐怖的Integer.MAX_VALUE,这个值已经很大了,因此可以将之称为无界队列。不过该队列也提供了有参构造函数,可以手动指定其队列大小,否则使用默认的int最大值。

LinkedBlockingQueue只能从head取元素,从tail添加元素。添加元素和获取元素都有独立的锁,也就是说它是读写分离的,读写操作可以并行执行。LinkedBlockingQueue采用可重入锁(ReentrantLock)来保证在并发情况下的线程安全。

当线程数目达到corePoolSize时,后续的任务会直接加入到LinkedBlockingQueue中,在不指定其队列大小的情况下,该队列永远也不会满,可能内存满了,队列都不会满,此时maximumPoolSize和拒绝策略将不会有任何意义

ArrayBlockingQueue

由数组实现的有界阻塞队列,同样遵循FIFO,必须制定队列大小。使用全局独占锁的方式,使得在同一时间只有一个线程能执行入队或出队操作,相比于LinkedBlockingQueue,ArrayBlockingQueue锁的力度很大。

SynchronousQueue

是一个没有容量的队列,当然也可以称为单元素队列。会将任务直接传递给消费者,添加任务时,必须等待前一个被添加的任务被消费掉,即take动作等待put动作,put动作等待take动作,put与take是循环往复的

如果线程拒绝执行该队列中的任务,或者说没有线程来执行。那么旧任务无法被执行,新任务也无法被添加,线程池将陷入一种尴尬的境地。因此,该队列一般需要maximumPoolSize为Integer.MAX_VALUE,有一个任务到来,就立马新起一个线程执行,newCachedThreadPool就是使用的这种组合。

关于这些阻塞队列的源码解析,可能需要另开篇幅。


线程工厂

先看一下,ThreadPoolExecutor构造方法中默认使用的线程工厂

static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() + "-thread-";
        } public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon())
                t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY); return t;
        }
    }

defaultThreadFactory对于线程的命名方式为“pool-”+pool的自增序号+"-thread-"+线程的自增序号,这也印证了在简单实例的章节中,输出Thread.getCurrentThread.getName()是“pool-1-thread-1”的样式

默认线程工厂给线程的取名没有太多的意义,在实际开发中,我们一般会给线程取个比较有识别度的名称,方便出现问题时的排查。


拒绝策略

如果当工作队列已满,且线程数目达到maximumPoolSize后,依然有任务到来,那么此时线程池就会采取拒绝策略。

ThreadPoolExecutor中提供了4种拒绝策略。

AbortPolicy

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " +
                                                 e.toString());
            }
    }

这是线程池的默认拒绝策略,直接会丢弃任务并抛出RejectedExecutionException异常。

DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

丢弃后续提交的任务,但不抛出异常。建议在一些无关紧要的场景中使用此拒绝策略,否则无法及时发现系统的异常状态。

DiscardOldestPolicy

public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

从源码中可以看到,此拒绝策略会丢弃队列头部的任务,然后将后续提交的任务加入队列中。

CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) {
                r.run();
            }
        }
    }

由调用线程执行该任务,即提交任务的线程,一般是主线程。


如何配置最大线程数

CPU密集型任务

CPU密集指的是需要进行大量的运算,一般没有什么阻塞。

尽量使用较小的线程池,大小一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务

IO密集指的是需要进行大量的IO,阻塞十分严重,可以挂起被阻塞的线程,开启新的线程干别的事情。

可以使用稍大的线程池,大小一般为CPU核心数*2。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

当然,依据IO密集的程度,可以在两倍的基础上进行相应的扩大与缩小。


总结

这篇文章粗浅地说明了线程池的种类、执行流程、工作队列与拒绝策略等,但缺少对线程池源码的分析,这个会另开篇幅进行说明。