Java中线程池简介
当程序中线程创建过多或频繁创建/删除线程,需要消耗服务器大量资源,且易造成OOM,所以人们,创建了线程池来管理线程的创建和销毁.
1 常见的四种创建线程池方法
使用Executors
类提供的方法,很容易获取线程池.
tips: alibaba
编码规范手册上不推荐使用该方法,建议使用ThreadPoolExecutor
方法,下章讲.
1 newFixedThreadPool(固定大小的线程池)
@Test
public void newFixedThreadPoolTest() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(new Date() +Thread.currentThread().getName());
//线程睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//线程睡眠 防止线程执行前就结束
Thread.sleep(3000);
executorService.shutdown();
}
运行结果:
Mon Mar 22 20:37:57 CST 2021pool-1-thread-1
Mon Mar 22 20:37:57 CST 2021pool-1-thread-2
Mon Mar 22 20:37:58 CST 2021pool-1-thread-1
说明: 创建一个线程数量为2的线程池,使用for循环提交了三个任务,每个任务睡眠一秒,前面只有两个任务拿到线程执行,后面等下一轮线程执行.且执行的时间差为1秒.
2 newCachedThreadPool(可缓存的线程池)
@Test
public void newCachedThreadPoolTest() throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(new Date() +Thread.currentThread().getName());
//线程睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//线程睡眠 防止线程执行前就结束
Thread.sleep(1000);
executorService.shutdown();
}
运行结果:
Mon Mar 22 22:06:06 CST 2021pool-1-thread-2
Mon Mar 22 22:06:06 CST 2021pool-1-thread-1
Mon Mar 22 22:06:06 CST 2021pool-1-thread-3
说明: 创建一个可以缓存的线程池,对线程数没有限制,可创建JVM能允许的最大线程数量.
3 newSingleThreadExecutor(单线程的线程池)
@Test
public void newSingleThreadExecutorTest() throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(new Date() +Thread.currentThread().getName());
//线程睡眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//线程睡眠 防止线程执行前就结束
Thread.sleep(4000);
executorService.shutdown();
}
运行结果:
Mon Mar 22 22:11:00 CST 2021pool-1-thread-1
Mon Mar 22 22:11:01 CST 2021pool-1-thread-1
Mon Mar 22 22:11:02 CST 2021pool-1-thread-1
说明: 单个线程执行所有任务,类似单线程执行.
4 newScheduledThreadPool(定时及周期性任务的线程池)
定时执行
@Test
public void newScheduledThreadPoolTest() throws InterruptedException {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
System.out.println("开始时间"+new Date());
executorService.schedule(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程执行时间"+new Date() + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1, TimeUnit.SECONDS);
//线程睡眠 防止线程执行前就结束
Thread.sleep(3000);
executorService.shutdown();
}
运行结果:
开始时间Mon Mar 22 22:24:18 CST 2021
线程执行时间Mon Mar 22 22:24:19 CST 2021pool-1-thread-1
说明: 创建一个固定大小为2的线程池,设置延迟1秒执行任务.
周期执行
@Test
public void newScheduledThreadPoolTest() throws InterruptedException {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println("开始时间"+new Date());
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程执行时间"+new Date() + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1,3, TimeUnit.SECONDS);
//线程睡眠 防止线程执行前就结束
Thread.sleep(8000);
executorService.shutdown();
}
运行结果:
开始时间Mon Mar 22 22:28:23 CST 2021
线程执行时间Mon Mar 22 22:28:24 CST 2021pool-1-thread-1
线程执行时间Mon Mar 22 22:28:27 CST 2021pool-1-thread-1
线程执行时间Mon Mar 22 22:28:30 CST 2021pool-1-thread-2
说明: 创建一个线程数为3的周期线程池,设置每个一秒执行一次任务,总共执行三次.
总结:
线程池类型 | 线程类型 | 数量 | 特点 | 应用 |
---|---|---|---|---|
newFixedThreadPool | 核心 | 固定 | 核心线程处于空闲状态,不会被回收 | 控制线程最大并发数 |
newCachedThreadPool | 核心&非核心 | 核心固定/非核心不固定 | 非核心线程闲置时,会被立即回收 | 执行定时/周期任务 |
newScheduledThreadPool | 非核心 | 不固定 | 优先使用闲置线程处理任务 / 无线程使用,则新建线程 / 灵活回收空置线程 | 执行量多,耗时少的任务 |
newSingleThreadExecutor | 核心 | 1 | 所有任务按照指定顺序在一个线程中执行 | 单线程 |
-
FixedThreadPool
和SingleThreadExecutor
线程池中,使用的是LinkedBlockingQueue
阻塞队列,请求处理堆积,可能会耗费很多内存,甚至OOM. -
CachedThreadPool
和ScheduledThreadPool
线程池,主要是线程最大数量是Integer.MAX_VALUE
,可能创建巨量的线程,导致OOM.
综上等原因: alibaba
编程手册建议不使用上述方法创建线程池.而是使用ThreadPoolExecutor
类创建线程池.
2 ThreadPoolExecutor类创建线程池
1 ThreadPoolExecutor常用构造方法
//构造1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
//构造2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
//构造3
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
//构造4
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明:
参数名称 | 参数说明 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 线程池最大线程数 |
workQueue | 任务队列 |
keepAliveTime | 线程闲置超时时间 |
TimeUnit | 指定keepAliveTime的时间单位 |
threadFactory(可选) | 线程工厂 |
handler(可选) | 拒绝策略 |
2 线程池参数说明
1 workQueue
任务队列: 基于阻塞队列实现的,采用生产者和消费者模式.
队列接口为BlockingQueue
,常见阻塞实现如下:
-
ArrayBlockingQueue 由数组结构组成的有界阻塞队列
-
LinkedBlockingQueue 链表结构组成的有界阻塞队列,未指定容量,默认Integer最大值
-
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
-
DelayQueue 无界优先级阻塞队列,通过执行时延从队列中提取任务
-
SynchronousQueue 不存储元素的阻塞队列
-
LinkedBlockingDeque 双向队列实现的有界双端阻塞队列
-
LinkedTransferQueue 无界的阻塞队列
tips:
-
有界: 当任务队列达到最大值且超过最大线程数,就会执行拒绝策略.
-
无界: 任务队列可以一直添加任务
2 threadFactory
线程工厂: 创建线程的方式.
需要实现ThreadFactory接口,且实现**newThread(Runnable r)**方法
Executors
中默认的线程工厂如下:
/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
3 handler
拒绝策略: 线程池的线程达到最大线程时,需要执行拒绝策略.
需要实现RejectedExecutionHandler接口,并实现**rejectedExecution(Runnable r, ThreadPoolExecutor executor)**方法.
Executors
中提供的4种拒绝策略:
- AbortPolicy 丢弃任务并抛出RejectedExecutionException异常
- CallerRunsPolicy:由调用线程处理该任务
- DiscardPolicy:丢弃任务,但是不抛出异常 (可以配合这种模式进行自定义的处理方式)
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
/**
* A handler for rejected tasks that runs the rejected task
* directly in the calling thread of the {@code execute} method,
* unless the executor has been shut down, in which case the task
* is discarded.
*/
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
3 线程池执行流程图
public class ThreadTest {
// 任务数
private static final int taskCount = 31;
public static void main(String[] args) {
AtomicInteger integer = new AtomicInteger();
// 创建一个线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
20,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(30),
r -> new Thread(r, "test-thread-" + integer.get()));
System.out.println("总任务数: " + taskCount);
long start = System.currentTimeMillis();
// 模拟任务提交
for (int i = 0; i < taskCount; i++) {
Thread thread = new Thread(() -> {
try {
// 模拟任务执行
Thread.sleep(500);
System.out.println("当前线程为: "+Thread.currentThread().getName()+"执行第" + integer.addAndGet(1) + "个任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
try {
// 拒绝策略会报错
executor.execute(thread);
} catch (Exception e) {
e.printStackTrace();
}
}
long end = 0;
while (executor.getCompletedTaskCount() < taskCount) {
end = System.currentTimeMillis();
}
System.out.println("任务总耗时: " + (end - start));
}
}
运行结果
/**
* 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
* 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
* 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
* 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
* 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常
*
*
* 记住一句话
* 任务数 <= 核心线程数时,线程池中工作线程数 = 任务数
* 核心线程数 + 队列容量 < 任务数 <= 最大线程数 + 队列容量时,工作线程数 = 任务数 - 队列容量
*
*
* 总任务数: 5
* 执行第1个任务
* 执行第5个任务
* 任务总耗时: 541 任务数小于核心线程数 , 任务立即执行 , 且是异步执行, 所以500ms 代码模拟需要花费时间, 所以总用时541
*
* 总任务数: 10
* 执行第1个任务
* 执行第10个任务
* 任务总耗时: 545 任务数等于核心线程数,没超过核心线程数, 任务立即执行 , 且是异步执行, 所以500ms
*
*
* 总任务数: 11
* 执行第1个任务
* 执行第11个任务
* 任务总耗时: 1046 任务数大于核心线程数,超过核心线程数, 10个任务立即执行 , 第11个任务会放到队列中, 等线程有空再执行, 即 10+1 总用时1000
*
*
* 总任务数: 20
* 执行第1个任务
* 执行第20个任务
* 任务总耗时: 1044 任务数大于核心线程数,超过核心线程数, 10个任务立即执行 , 剩下的任务会放到队列中, 等线程有空再执行, 即 10+10 总用时1000
*
*
* 总任务数: 30
* 执行第1个任务
* 执行第30个任务
* 任务总耗时: 1547 任务数大于核心线程数,超过核心线程数, 10个任务立即执行 , 剩下的任务会放到队列中, 等线程有空再执行,每次都执行十个 即 10+10+10 总用时1500
*
* 总任务数: 35
* 执行第1个任务
* 执行第35个任务
* 任务总耗时: 2051 任务数大于核心线程数,超过核心线程数, 10个任务立即执行 , 剩下的任务会放到队列中, 等线程有空再执行,每次都最大 即 10+10+10+5 总用时2000
*
* 总任务数: 40
* 执行第1个任务
* 执行第40个任务
* 任务总耗时: 2070 任务数大于核心线程数,超过核心线程数, 10个任务立即执行 , 剩下的任务会放到队列中, 等线程有空再执行,每次都最大 即 10+10+10+10 总用时2000
*
*
* 总任务数: 41
* 执行第1个任务
* 执行第41个任务
* 任务总耗时: 2056 任务数大于 (核心线程数 + 队列数) , 每次执行为 41-30 = 11, 执行计划 11+11+11+8 总用时2000
*
*
*
* 总任务数: 44
* 执行第1个任务
* 执行第44个任务
* 任务总耗时: 2058 任务数大于 (核心线程数 + 队列数) , 每次执行为 44-30 = 14, 执行计划 14+14+14+2 总用时2000
*
*
* 总任务数: 50
* 执行第1个任务
* 执行第50个任务
* 任务总耗时: 1558 任务数大于 (核心线程数 + 队列数) , 每次执行为 50-30 = 20, 执行计划 20+20+10 总用时 1500
*
*/
参考资料:
https://blog.csdn.net/qq_44918331/article/details/114372078
https://jimmysun.blog.csdn.net/article/details/95225769
https://blog.csdn.net/qq_32748869/article/details/107511616