别再说你不懂线程池——作个优雅的攻城狮

  • 什么是线程池
  • 为何要使用线程池
  • 线程池的处理逻辑
  • 如何使用线程池
  • 如何合理配置线程池的大小
  • 结语

什么是线程池

线程池,顾名思义就是装线程的池子。其用途是为了帮咱们重复管理线程,避免建立大量的线程增长开销,提升响应速度。 java

为何要用线程池

做为一个严谨的攻城狮,不会但愿别人看到咱们的代码就开始吐槽,new Thread().start()会让代码看起来混乱臃肿,而且很差管理和维护,那么咱们就须要用到了线程池。编程

在编程中常常会使用线程来异步处理任务,可是每一个线程的建立和销毁都须要必定的开销。若是每次执行一个任务都须要开一个新线程去执行,则这些线程的建立和销毁将消耗大量的资源;而且线程都是“各自为政”的,很难对其进行控制,更况且有一堆的线程在执行。线程池为咱们作的,就是线程建立以后为咱们保留,当咱们须要的时候直接拿来用,省去了重复建立销毁的过程。异步

线程池的处理逻辑

线程池ThreadPoolExecutor构造函数

//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) //六个参数的构造函数-1 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) //六个参数的构造函数-2 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) //七个参数的构造函数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 复制代码

虽然参数多,只是看着吓人,其实很好理解,下面会一一解答。函数

咱们拿最多参数的来讲:spa

1.corePoolSize -> 该线程池中核心线程数最大值

核心线程:在建立完线程池以后,核心线程先不建立,在接到任务以后建立核心线程。而且会一直存在于线程池中(即便这个线程啥都不干),有任务要执行时,若是核心线程没有被占用,会优先用核心线程执行任务。数量通常状况下设置为CPU核数的二倍便可。线程

2.maximumPoolSize -> 该线程池中线程总数最大值

线程总数=核心线程数+非核心线程数3d

非核心线程:简单理解,即核心线程都被占用,但还有任务要作,就建立非核心线程code

3.keepAliveTime -> 非核心线程闲置超时时长

这个参数能够理解为,任务少,但池中线程多,非核心线程不能白养着,超过这个时间不工做的就会被干掉,可是核心线程会保留。cdn

4.TimeUnit -> keepAliveTime的单位

TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天blog

5.BlockingQueue workQueue -> 线程池中的任务队列

默认状况下,任务进来以后先分配给核心线程执行,核心线程若是都被占用,并不会马上开启非核心线程执行任务,而是将任务插入任务队列等待执行,核心线程会从任务队列取任务来执行,任务队列能够设置最大值,一旦插入的任务足够多,达到最大值,才会建立非核心线程执行任务。

常见的workQueue有四种:

1.SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,若是全部线程都在工做怎么办?那就新建一个线程来处理这个任务!因此为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize通常指定成Integer.MAX_VALUE,即无限大

2.LinkedBlockingQueue:这个队列接收到任务的时候,若是当前已经建立的核心线程数小于线程池的核心线程数上限,则新建线程(核心线程)处理任务;若是当前已经建立的核心线程数等于核心线程数上限,则进入队列等待。因为这个队列没有最大值限制,即全部超过核心线程数的任务都将被添加到队列中,这也就致使了maximumPoolSize的设定失效,由于总线程数永远不会超过corePoolSize

3.ArrayBlockingQueue:能够限定队列的长度,接收到任务的时候,若是没有达到corePoolSize的值,则新建线程(核心线程)执行任务,若是达到了,则入队等候,若是队列已满,则新建线程(非核心线程)执行任务,又若是总线程数到了maximumPoolSize,而且队列也满了,则发生错误,或是执行实现定义好的饱和策略

4.DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

6.ThreadFactory threadFactory -> 建立线程的工厂

能够用线程工厂给每一个建立出来的线程设置名字。通常状况下无须设置该参数。

7.RejectedExecutionHandler handler -> 饱和策略

这是当任务队列和线程池都满了时所采起的应对策略,默认是AbordPolicy, 表示没法处理新任务,并抛出 RejectedExecutionException 异常。此外还有3种策略,它们分别以下。
(1)CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,可以减缓新任务的提交速度。
(2)DiscardPolicy:不能执行的任务,并将该任务删除。
(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

别晕,接下来上图,相信结合图你能大彻大悟~

如何使用线程池

说了半天原理,接下来就要用了,java为咱们提供了4种线程池FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool,几乎能够知足咱们大部分的须要了:

1.FixedThreadPool

可重用固定线程数的线程池,超出的线程会在队列中等待,在Executors类中咱们能够找到建立方式:

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

FixedThreadPoolcorePoolSizemaximumPoolSize都设置为参数nThreads,也就是只有固定数量的核心线程,不存在非核心线程。keepAliveTime为0L表示多余的线程马上终止,由于不会产生多余的线程,因此这个参数是无效的。FixedThreadPool的任务队列采用的是LinkedBlockingQueue。

建立线程池的方法,在咱们的程序中只须要,后面其余种类的同理:

public static void main(String[] args) {
        // 参数是要线程池的线程最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
}
复制代码

2.CachedThreadPool

CachedThreadPool是一个根据须要建立线程的线程池

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

CachedThreadPoolcorePoolSize是0,maximumPoolSize是Int的最大值,也就是说CachedThreadPool没有核心线程,所有都是非核心线程,而且没有上限。keepAliveTime是60秒,就是说空闲线程等待新任务60秒,超时则销毁。此处用到的队列是阻塞队列SynchronousQueue,这个队列没有缓冲区,因此其中最多只能存在一个元素,有新的任务则阻塞等待。

3.SingleThreadExecutor

SingleThreadExecutor是使用单个线程工做的线程池。其建立源码以下:

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

咱们能够看到总线程数和核心线程数都是1,因此就只有一个核心线程。该线程池才用链表阻塞队列LinkedBlockingQueue,先进先出原则,因此保证了任务的按顺序逐一进行。

4.ScheduledThreadPool

ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的建立源码以下:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
复制代码

这里建立了ScheduledThreadPoolExecutor,继承自ThreadPoolExecutor,主要用于定时延时或者按期处理任务。ScheduledThreadPoolExecutor的构造以下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
复制代码

能够看出corePoolSize是传进来的固定值,maximumPoolSize无限大,由于采用的队列DelayedWorkQueue是无解的,因此maximumPoolSize参数无效。该线程池执行以下:

当执行 scheduleAtFixedRate或者 scheduleWithFixedDelay方法时,会向 DelayedWorkQueue添加一个实现 RunnableScheduledFuture接口的 ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到 corePoolSize。若是没有则新建线程并启动 ScheduledFutureTask,而后去执行任务。若是运行的线程达到了 corePoolSize时,则将任务添加到 DelayedWorkQueue中。 DelayedWorkQueue会将任务进行排序,先要执行的任务会放在队列的前面。在跟此前介绍的线程池不一样的是,当执行完任务后,会将 ScheduledFutureTask中的 time变量改成下次要执行的时间并放回到 DelayedWorkQueue中。

如何合理配置线程池的大小

通常须要根据任务的类型来配置线程池大小:
若是是CPU密集型任务,就须要尽可能压榨CPU,参考值能够设为 NCPU+1
若是是IO密集型任务,参考值能够设置为2*NCPU
固然,这只是一个参考值,具体的设置还须要根据实际状况进行调整,好比能够先将线程池大小设置为参考值,再观察任务运行状况和系统负载、资源利用率来进行适当调整。

结语

java为咱们提供的线程池就介绍到这了,墙裂建议你们仍是动手去敲一敲,毕竟实践过内心才有底。

相关文章
相关标签/搜索