Java里 Thread类 就是学习Java了解线程的开始, 但是实际业务编程中, 很少直接使用 Thread 这个类来创建线程 [new Thread]
, 下面直接给出原因 .
- 每次
new Thread
新建对象, 程序性能会很差 - 线程缺乏统一的管理 , 可能会无限制的创建新线程 , 相互竞争, 有可能会占用过多的系统资源,导致死机或者
OOM
- 缺少更多的功能 , 如更多的执行,定期的执行,线程的中端
那么既然 直接使用 Thread
不好, 那么使用线程池的优点又是什么, 我们为什么要用它 , 它究竟有什么?
- 使用线程池可以重用存在的线程, 减少对象创建,消亡的开销, 提升系统的性能
- 可以有效控制最大并发线程数 , 提高系统资源利用率, 同时可以避免过多资源竞争,避免阻塞
- 可提供定时执行,定期执行,单线程,并发线程控制等等
我们来看一下线程池相关的一个类 ThreadPoolExecutor
首先看一下ThreadPoolExecutor
都接收哪些参数
corePoolSize
核心线程数量maximumPoolSize
线程最大线程数workQueue
阻塞队列, 存储等待执行的任务, 很重要的一个参数, 会对线程池运行过程产生重大影响
好了 , 那它们几个的关系是什么呢? 首先 ,如果运行的线程数量小于 corePoolSize
那么程序直接创建新线程处理任务 , 即使线程池中的其他线程是空闲的 ; 如果运行的线程数量 >= corePoolSize
并且 < maximumPoolSize
时 , 只有当workQueue
满的时候,才创建新的线程去处理任务 ; 如果 corePoolSize = maximumPoolSize
,当 workQueue
没有满的时候, 就把请求放在 workQueue
里, 等线程池的空闲线程来处理 ; 如果运行的线程数量 > maximumPoolSize
并且 workQueue
也满了,就要通过一个 [拒绝策略]
的方式来处理新的请求
对于 workQueue
上面说了, 它是用来保存执行任务的阻塞队列.当提交一个新的请求到线程池的时候, 线程池会根据当前线程池正在运行的线程数量来决定该请求的处理方法.处理的方式主要有 3 种
- 直接切换
SynchronousQueue
使用无界队列
LinkedBlockingQueue
使用这种方式, 线程池中能够创建的最大线程数就是corePoolSize
,maximumPoolSize
就不会起作用了,当所有的核心线程都处于运行状态时候, 一个新的请求过来, 就会放入 等待队列workQueue
使用有界队列
ArrayBlockingQueue
使用这种方式,线程池中能够创建的最大线程数就是maximumPoolSize
,这样能够降低资源的消耗, 但是这种方式使得线程池对线程的调度更加困难,因为线程池和队列都是有限的.
要想使得线程池处理任务和吞吐率处于一个合理的范围, 又想使得线程池调度简单, 并且还能尽可能降低对于资源的消耗, 就需要合理的设置 corePoolSize 和 maximumPoolSize
具体该怎么做呢?
如果想降低资源的消耗, [包括CPU使用率, 系统资源的消耗,上下文环境切换的开销等]
, 那么可以设置一个较大的队列容量和较小的线程池容量
这样会降低线程处理任务的吞吐量 .
如果提交的请求经常发生阻塞, 可以重新设置 maximumPoolSize
的容量 ,设置更多一些.
如果队列较小
, 那就需要将线程池容量设置更大一些,这样cpu 使用率会高一些 ,但是如果线程容量设置太大,请求太多的情况下, 并发会增大 ,线程吞吐量可能会下降. 所以在实际生产过程中, 这是一个不断的找平衡的需求.
含有全部参数的 ThreadPoolExecutor 构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
从上面的构造函数 , 我们看到 还有其他几个参数
keepAliveTime
线程没有任务执行时,最多保持多久停止unit
keepAliveTime 的时间单位threadFactory
线程工厂,用来创建线程handler
拒绝策略 ,当拒绝处理请求时的策略
keepAliveTime
当线程池中的线程数量 > corePoolSize
,如果这时没有新的请求 ,核心线程外的线程不会立即销毁, 而是在线程池里等待 , 直到等待时间超过 keepAliveTime
handler
如果之前说的阻塞队列已满, 并且没有空闲的线程时, 这时还在继续提交请求, 这时线程池就需要来处理继续提交的请求, 总共有 4 中策略 ,直接给出
- 直接抛出异常 [默认的策略]
- 用调用者所在的线程来执行任务
- 丢弃队列中最靠前的任务,并执行当前任务
- 直接丢弃该任务