前言
对于线程池,我们需要更多关注的是
ThreadPoolExecutor
这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的,对于这个类也是需要更深入的学习
1. 常用参数详解
从 ThreadPoolExecutor 类的一个构造方法来看
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize, long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.1 核心参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
当一个任务提交至线程池后的处理逻辑:
- 线程池首先当前运行的线程数量是否少于
corePoolSize
,如果是,则创建一个新的工作线程来执行任务。 - 如果没有空闲的线程执行该任务且当前的线程数等于
corePoolSize
同时BlockingQueue
未满,则将任务加入BlockingQueue
,而不添加新的线程。 - 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于
maximumPoolSize
,则创建新的线程执行任务。 - 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于
maximumPoolSize
,则根据构造函数中的handler
指定的策略来拒绝新的任务。
另:
- 线程池并没有标记哪个线程是核心线程,哪个是非核心线程,线程池只关心核心线程的数量。
- 当
ThreadPoolExecutor
创建新线程时,通过CAS
来更新线程池的状态
1.2 其他常见参数:
-
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁 -
unit
:keepAliveTime
参数的时间单位 -
threadFactory
:executor 创建新线程的时候会用到 -
handle
:饱和策略
ThreadPoolExecutor 饱和策略定义
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理ThreadPoolExecutor.CallerRunsPolicy
:由向线程池提交任务的线程来执行该任务ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉ThreadPoolExecutor.DiscardOldestPolicy
: 丢弃最早提交但是未处理的任务
2. ThreadPoolExecutor 例子
创建一个任务,该任务是 Runnable 类型
public class RunnableTask implements Runnable {
//用来标识当前线程所运行的任务
int number;
public ThreadTask(int number) {
this.number = number;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " Start -- " + number);
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName() + " End -- " + number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用 ThreadPoolExecutor 线程池运行任务
public class ThreadPoolTest {
/** 线程池对应参数 */
private static final Integer corePoolSize = 4;
private static final Integer maximumPoolSize = 8;
private static final Long keepAliveTime = (long) 8;
private static final TimeUnit timeUnit = TimeUnit.SECONDS;
private static final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(16);
private static final RejectedExecutionHandler handle = new ThreadPoolExecutor.AbortPolicy();
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, handle);
for (int i = 0; i < 4; i++) {
RunnableTask runnableTask = new RunnableTask(i);
threadPoolExecutor.execute(threadTask);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
示例中创建了固定大小为四的线程池,然后分配了五个工作,因为线程池大小为四,它将启动四个工作线程先处理四个工作,其他的工作则处于等待状态,一旦有工作完成,空闲下来工作线程就会拾取等待队列里的其他工作进行执行。这四个线程会一直存在,直到线程池消亡。
以下是程序的运行输出:
pool-1-thread-4 Start -- 3
pool-1-thread-1 Start -- 0
pool-1-thread-2 Start -- 1
pool-1-thread-3 Start -- 2
pool-1-thread-4 End -- 3
pool-1-thread-1 End -- 0
pool-1-thread-4 Start -- 4
pool-1-thread-2 End -- 1
pool-1-thread-3 End -- 2
pool-1-thread-4 End -- 4
Finished all threads
我们不仅可以在创建 ThreadPoolExecutor 实例时指定活动线程的数量,也可以限制线程池的大小并且创建我们自己的 RejectedExecutionHandler 实现来处理被拒绝的任务
自定义拒绝处理器:
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + "被拒绝了");
}
}
将 ThreadPoolTest.java 中的线程池参数稍微改动一下,将阻塞队列减少为1,由于线程池最大为8个,阻塞队列只能存放一个任务,当提交10个任务时,将有1个任务被自定义的饱和策略拒绝
public class ThreadPoolTest {
/** 线程池对应参数 */
private static final Integer corePoolSize = 4;
private static final Integer maximumPoolSize = 8;
private static final Long keepAliveTime = (long) 8;
private static final TimeUnit timeUnit = TimeUnit.SECONDS;
/** 减少了阻塞队列 */
private static final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1);
/** 设置为自己的实现 */
private static final RejectedExecutionHandler handle = new RejectedExecutionHandlerImpl();
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, handle);
//提交十个任务
for (int i = 0; i < 10; i++) {
ThreadTask threadTask = new ThreadTask(i);
threadPoolExecutor.execute(threadTask);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
执行结果:
cn.testproject.thread.runnable.ThreadTask@457e2f02被拒绝了
pool-1-thread-1 Start -- 0
pool-1-thread-3 Start -- 2
pool-1-thread-5 Start -- 5
pool-1-thread-7 Start -- 7
pool-1-thread-4 Start -- 3
pool-1-thread-2 Start -- 1
pool-1-thread-8 Start -- 8
pool-1-thread-6 Start -- 6
pool-1-thread-4 End -- 3
pool-1-thread-3 End -- 2
pool-1-thread-1 End -- 0
pool-1-thread-7 End -- 7
pool-1-thread-8 End -- 8
pool-1-thread-5 End -- 5
pool-1-thread-4 Start -- 4
pool-1-thread-6 End -- 6
pool-1-thread-2 End -- 1
pool-1-thread-4 End -- 4
Finished all threads
3. 推荐使用 ThreadPoolExecutor 构造函数创建线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回的线程池允许创建的线程数量为 Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。
4. 线程池设置设置多大比较合适
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
5. 线程池中任务是如何关闭的?
线程池使用 shutdown()
和 shutdownNow()
方法关闭任务
-
shutdownNow()
: 线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行 -
shutdown()
:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池