java 多线程开发系列之九:玩转线程池(线程池的简介和简单使用)

线程池是什么?

顾名思义,就是线程池的一个池子,它是java程序执行时对需要调用的线程进行一个预存。是池化技术的代表,除此之外,常用的池化技术还有连接池,字符串常量池等。

它们的存在一般是为了解决两个问题:

1,降低由于创建或销毁(线程/链接)的开销。

2,方便统一调配和管理池中的资源。

所谓创建/销毁资源的开销通常是指:

我们知道,一个java中new 出来的线程,需要在cpu中创建一个线程,来一一对应该java线程。

在执行完runnable中的run方法后。又需要cpu去销毁自身的一个线程。这cpu一创建一销毁的性能开销就比较大了。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这不仅仅cpu 执行一个指令就行,还需要分配资源,内核态切换上下文切换等,实际成本非常的高。在高并发或者是大量多线程协作场景中,如果还是这么用,那么大量的性能开销都浪费在cpu的资源调度上了。这显然是我们所不愿意看到的。

解决办法也很简单,就是让线程所执行的run方法都连续在一起,执行完某一个run方法,线程不结束,还继续等待新的run方法。

实际工作中,我们往往需要涉及到使用多个线程来执行一个庞大的被分解成若干子任务的情况。此时就需要new 出多个线程,然后去执行一个庞大的待执行的任务集合。

读到这里,你就懂了线程池的基本设计思想了。

而所谓的调配和管理池中资源

主要是为了方便对线程统一创建(线程工厂),线程执行任务进行统计(执行每个run方法进行埋点,统计)。

接下来我们详细说下线程池常用的参数。

首先我们需要设定一个池中,稳定线程的数量。

这就是常说的核心线程数

corePoolSize

考虑到池中的工作任务可能非常多,我们需要有一个集合来存放还未来得及执行的任务。也就是前文中提到的各种run方法中的逻辑。

这就是常说的任务队列(为了能和线程协作起来,一般我们使用阻塞队列)

taskQueue

假设任务队列定死,核心线程数也定死。但是很多业务场景中,待处理任务不是固定大小的,是有峰谷的。比如白天数据量很多,夜间数据量很少。此时如果线程数固定死,整体就很死板,没有弹性,夜间的性能就浪费了。

所以我们还需要一个弹性线程数。

但是我们往往不直接设置这个线程数,而是给一个总线程数,这个总线程数=核心线程数+弹性线程数

maxPoolSize

这些弹性的线程,有一个专属的名称,非核心线程。这里又涉及到一个问题,峰谷并不是两个状态,整体的任务数曲线可能存在若干的抖动。此时非核心线程数也不能用完就立刻销毁,否则一样有性能问题。

此时就需要给非核心线程创建一个最大空闲时间。也就是当它们没有任务执行时,最大能空闲多久,如果空闲期间仍然没有任务要执行,此时才会被销毁。

keepAliveTime (空闲时间大小)

unit (空闲时间单位)

另外此外我们还需要有一个工厂类,用来统一创建工厂,方便管理。称之为线程工厂

threadFactory (这个类实际用的不多,大部分是用来统一设置线程名称的)

除此之外,我们还要考虑健壮性,假设待执行任务非常多,线程池根本就容纳不了,也执行不动,我们为了考虑到线程池本身需要正常的运行,需要对额外的线程进行拒绝。但是拒绝方式各种各样,所以还需要允许开发者设置一个拒绝策略。

RejectedExecutionHandler handler

至此线程池的常用配置我们就了解清楚了。但是实际线程池的设计和实现要复杂的多。

接下来看下java线程池类的类继承关系,java自带线程池的继承关系非常简单,如图:

基础接口是Executor

它只声明了一个执行接口execute,参数是runnable

扩展接口是ExecutorService

表示是一个执行器服务,增加了shutdown终止方法,submit支持返回值接口,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )支持批量执行的invokexx方法

抽象类 AbstractExecutorService

对submit,invokexx等方法进行了模板方法实现。但是底层的execute方法仍然没有实现

具体实现类 ThreadPoolExecutor

完整的实现了execute方法,实现了各种工作线程/终止条件条件/锁/任务调度/终止等方法逻辑

整体看下来,其实线程池的主要实现还是在ThreadPoolExecutor。父对象中,主要还是在声明一个执行器相关的行为。

接着我们来看一个线程池的例子:

复制代码
 1 package org.example;
 2 
 3 import java.util.Date;
 4 import java.util.concurrent.BlockingQueue;
 5 import java.util.concurrent.LinkedBlockingQueue;
 6 import java.util.concurrent.ThreadPoolExecutor;
 7 import java.util.concurrent.TimeUnit;
 8 import java.util.concurrent.atomic.AtomicInteger;
 9 
10 /**
11  * @discription
12  */
13 public class ThreadPoolStudy {
14     private static AtomicInteger count = new AtomicInteger();
15 
16     private static int coreThreadSize = 5;
17 
18     private static int maxThreadSize = 10;
19 
20     private static long keepAliveTime = 60;
21 
22     private static TimeUnit timeUnit = TimeUnit.SECONDS;
23 
24     private static BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10);
25 
26     public static void main(String[] args) {
27         ThreadPoolExecutor executor = new ThreadPoolExecutor(coreThreadSize, maxThreadSize, keepAliveTime,
28                 timeUnit, taskQueue);
29         long start = new Date().getTime();
30 
31         for (int i = 0; i < 30; i++) {
32             try {
33                 executor.execute(() -> {
34                     int value = count.incrementAndGet();
35                     System.out.println("thread :"+ Thread.currentThread().getName()+" |current value:" + value);
36                     try {
37                         TimeUnit.SECONDS.sleep(1);
38                     } catch (InterruptedException e) {
39                         //do sth
40                     }
41                 });
42             } catch (Exception ex) {
43                 System.out.println("exec catch ex :" + ex.getMessage());
44             }
45 
46         }
47         long end = new Date().getTime();
48         System.out.println("time cost :" + (end - start));
49     }
50 }

输出结果如下:

复制代码
 1 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 2 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 3 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 4 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 5 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 6 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 7 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 8 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
 9 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
10 exec catch ex :Task org.example.ThreadPoolStudy$$Lambda$15/0x000002d794001200@32a1bec0 rejected from java.util.concurrent.ThreadPoolExecutor@22927a81[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
11 time cost :7
12 thread :pool-1-thread-4 |current value:3
13 thread :pool-1-thread-5 |current value:4
14 thread :pool-1-thread-9 |current value:10
15 thread :pool-1-thread-8 |current value:9
16 thread :pool-1-thread-3 |current value:5
17 thread :pool-1-thread-1 |current value:1
18 thread :pool-1-thread-6 |current value:6
19 thread :pool-1-thread-7 |current value:7
20 thread :pool-1-thread-10 |current value:8
21 thread :pool-1-thread-2 |current value:2
22 thread :pool-1-thread-4 |current value:11
23 thread :pool-1-thread-5 |current value:12
24 thread :pool-1-thread-8 |current value:14
25 thread :pool-1-thread-3 |current value:16
26 thread :pool-1-thread-7 |current value:13
27 thread :pool-1-thread-6 |current value:15
28 thread :pool-1-thread-10 |current value:17
29 thread :pool-1-thread-9 |current value:18
30 thread :pool-1-thread-1 |current value:19
31 thread :pool-1-thread-2 |current value:20

首先我们发现输出是乱序的,也就是线程拉起虽然有先后顺序,但是真正执行起来,究竟是谁先谁后就不一定了。这符合我们对多线程并发的理解。

其次我们发现循环提交执行任务的速度很快,只有7个ms,

在这7ms内,共有20个任务被成功提交,其中包含10个线程正在执行的任务,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )10个堆积到任务队列中等待执行。我们一共提交了30个任务,无法提交的任务

被抛出异常。所以线程池理论上在某一瞬间所能承载的总任务量=总线程数+阻塞队列支持的最大容量。

另外还需要特别注意到的是:

创建线程池后,池中本身是没有线程的。当有任务被提交时,才会开始创建线程。

当线程数到达核心线程数上限时,如果仍然有新的任务提交,则会将任务放置到任务队列中,直至任务队列满为止。

如果此时继续提交任务,才会继续创建线程,直至线程数到达最大线程数。

所以我们可以认为线程池中的线程默认是一种懒启动策略。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )

如果任务队列满仍然还提交时,才会继续创建非核心线程。(即核心线程和任务队列是第一优先级,非核心线程是第二优先级,当第一优先级无处可放时,才需要第二优先级来处理)。

当非核心线程创建满时,无法还有任务提交,就会执行拒绝策略(不要把拒绝策略想的太神秘,其实就是一个模板接口,后文详细介绍)。

而工作线程执行完任务时,会去任务队列中取数据。如果任务队列没数据,工作线程就处于阻塞中。

还记得我前文关于阻塞队列的文章吗?不清楚可看:

***《生产者消费者模式,以及基于BlockingQueue的快速实现》*https://www.cnblogs.com/jilodream/p/18383435

这样咱们的技术点是不是就串联起来了。

此时非核心线程如果阻塞的时间超过最大空闲时间,才会自动终止该线程。