线程池
线程资源必须通过线程池提供,不应该在线程中自行显示的创建线程。
线程池能减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致内存被消耗完或者“CPU过度切换”的问题。
线程池尽量不要使用Executors去创建,而应使用ThreadPoolExecutors的方式创建自定义的线程池。
Executors创建线程池
Executors创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个:
- newFiexedThreadPool(int Threads)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 它是一种固定大小的线程池,corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
- keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
- 阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
- 由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
- newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 它是一个可以无限扩大的线程池;它比较适合处理执行时间比较小的任务;
- corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
- keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
- 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
- newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 它只会创建一条工作线程处理任务;
- 采用的阻塞队列为LinkedBlockingQueue;
-
newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,用来处理延时任务或定时任务,多数情况下可用来替代Timer类。 -
newWorkStealingPool()
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
- 它使用了一个无限队列来保存需要执行的任务,
- 而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么会取当前计算机可用的CPU数量作为线程数量
- Executors为什么存在缺陷
Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
-
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
-
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
而newFixedThreadPool和newSingleThreadExecutor中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
而newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。
正确的创建线程池方式:ThreadPoolExecutors创建自定义线程池
- 构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
名称 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数数 |
maximumPoolSize | int | 最大线程池数 |
keepAliveTime | long | 线程最大空闲时间 |
unit | TimeUnit | 时间单位 |
workQueue | BlockingQueue | 线程等待队列 |
threadFactory | ThreadFactory | 线程创建工厂 |
handler | RejectedExecutionHandler | 拒接策略 |
拒绝策略 | 拒绝行为 |
---|---|
AbortPolicy | 抛出RejectedExecutionException |
DiscardPolicy | 什么也不做,直接忽略 |
DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
CallerRunsPolicy | 直接由提交任务者执行这个任务 |
- 线程池工作流程:
- 创建多少个线程合适
CPU 密集型任务,理论上 线程数量 = CPU 核数(逻辑) 就可以了,但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1
I/O 密集型任务,理论上
最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时))
但是实际上,线程数量一般会设置为 2 x CPU 核数(逻辑)+ 1
参考 https://mp.weixin.qq.com/s/0WzUU-4r164jY7M5NTmEYg