Java线程Thread类,所有的线程对象都必须是Thread类或其子类的实例,Java可以用三种方式来创建线程:
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
做一些简单的操作开启一个线程处理数据可通过上面方式来完成,但是频换创建销毁线程的话还是要用线程池更节省资源,降低系统开销而且还能提供定时执行,定期执行,单线程,并发控制等功能。
线程池的本质是对任务和线程的管理,做到了将任务和线程两者解耦。线程池对任务的管理可看作生产者消费者的关系,通过阻塞队列的存与取。阻塞队列缓存待执行的任务,工作线程从阻塞队列中获取任务。线程池对线程的管理,是结合线程池状态,已有线程的状态,核心线程数和最大线程数、阻塞队列状态做出增加、执行任务、回收、复用等操作,体现了享元模式和池化思想。
想必很多人都看过阿里开发手册,里面提到一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。Executors可创建三类线程池:
- 创建返回ThreadPoolExecutor对象
- 创建返回ScheduleThreadPoolExecutor对象
- 创建返回ForkJoinPool对象
本篇只讨论常用的ThreadPoolExecutor对象。
Executors创建返回ThreadPoolExecutor对象:- Executors#newCachedThreadPool => 创建可缓存的线程池
- Executors#newSingleThreadExecutor => 创建单线程的线程池
- Executors#newFixedThreadPool => 创建固定长度的线程池
ps: ScheduledThreadPoolExecuto是ThreadPoolExecutor的子类,创建过程也是调用的父类的构造方法
不建议Executors创建线程池的原因:public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
提交任务时,corePoolSize因为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队列永远是满的,因此最终会创建非核心线程来执行任务。对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是无限创建线程,在服务器资源有限的情况下容易引起OOM异常。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new linkedBlockingQueue()));
}
任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为linkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize和keepAliveTime参数将无效,压根就不会创建非核心线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new linkedBlockingQueue());
}
和SingleThreadExecutor类似,由于使用的是linkedBlockingQueue,在资源有限的时候容易引起OOM异常。该线程池只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。现行大多数GUI程序都是单线程的。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
创建一个定长线程池,支持定时及周期性任务执行。ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。DelayedWorkQueue长度16,最大线程数因为是Integer.MAX_VALUE非常大,可以认为是无限创建线程,在服务器资源有限的情况下容易引起OOM异常。
总结下各类线程池的配置,以及其阻塞队列类型,使用场景。
| 类型 | 核心线程数 | 最大线程数 | 阻塞队列 | 说明/使用场景 |
|---|---|---|---|---|
| FixedThreadPool | 构造时传入 | 与核心线程数相同 | linkedBlockingQueue | 线程数量固定,只有核心线程并且不会被回收,没有超时机制 |
| CachedThreadPool | 0 | Integer.MAX_VALUE | SynchronousQueue | 线程数量不固定的线程池,只有非核心的线程,当线程都处于活动状态时,直接创建新线程来处理新任务,否则就利用空闲的线程。处于空闲状态超过60s的线程被回收 |
| ScheduledThreadPool | 构造时传入 | Integer.MAX_VALUE | DelayedWorkQueue | 非核心线程在闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务 |
| SingleThreadExecutor | 1 | 1 | linkedBlockingQueue | 只有一个核心线程,确保所有任务在同一线程中按顺序执行 |
自定义线程池ThreadPoolExecutor对象
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.tonanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
整个任务工作流程如下:
线程池的拒绝策略所有拒绝策略都实现了接口 RejectedExecutionHandler,类型如下:
AbortPolicy:默认拒绝策略,中断调用者的处理过程,直接抛出拒绝异常RejectedExecutionException
CallerRunsPolicy:只会用调用者所在线程来运行任务,也就是说任务不会进入线程池。如果线程池已经被关闭,则直接丢弃该任务。
DiscardOledestPolicy:丢弃队列中最老的,然后再次尝试提交新任务。
DiscardPolicy:默默丢弃无法加载的任务。
通过实现 RejectedExecutionHandler 自定义
核心线程数如何设置?在数设置之前需要说下两个概念,CPU密集型(计算密集型),IO密集型。
CPU密集型:是指系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。对于CPU密集型计算,多线程本质上是提升多核cpu的利用率,比如8核cpu,每核一个线程,理论上8个线程就可以了,如果设置过多的线程,实际上并不会起到很好的效果,反而由于线程数都想去利用cpu资源导致不必要的上下文切换,导致性能下降。
CPU密集任务:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。
IO密集型:是指系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
IO 密集型任务:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。
核心线程数
CPU密集型:核心线程数=CPU核心数(或 核心线程数=CPU核心数+1)
I/O密集型:核心线程数=2*CPU核心数(或 核心线程数=CPU核心数/(1-阻塞系数))
混合型:核心线程数=(线程等待时间/线程CPU时间+1)*CPU核心数



