Java 并发编程 | 线程池详解
原文: https://chenmingyu.top/concurrent-threadpool/
线程池
线程池用来处理异步任务或者并发执行的任务
优点:
- 重复利用已创建的线程,减少创建和销毁线程造成的资源消耗
- 直接使用线程池中的线程,提高响应速度
- 提高线程的可管理性,由线程池同一管理
ThreadPoolExecutor
java
中线程池使用ThreadPoolExecutor
实现
构造函数
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;
}
入参:
corePoolSize
:线程池的核心线程数量线程池维护的核心线程数量,当线程池初始化后,核心线程数量为零,当有任务来到的时候才会创建线程去执行任务,当线程池中的工作线程数量等于核心线程数量时,新到的任务就会放到缓存队列中
maximumPoolSize
:线程池允许创建的最大线程数量当阻塞队列满了的时候,并且线程池中创建的线程数量小于
maximumPoolSize
,此时会创建新的线程执行任务keepAliveTime
:线程活动保持时间只有当线程池数量大于核心线程数量时,
keepAliveTime
才会有效,如果当前线程数量大于核心线程数量时,并且线程的空闲时间达到keepAliveTime
,当前线程终止,直到线程池数量等于核心线程数unit
:线程活动保持时间的单位keepAliveTime
的单位,包括:TimeUnit.DAYS
天,TimeUnit.HOURS
小时,TimeUnit.MINUTES
分钟,TimeUnit.SECONDS
秒,TimeUnit.MILLISECONDS
毫秒,TimeUnit.MICROSECONDS
微秒,TimeUnit.NANOSECONDS
纳秒workQueue
:任务队列,用来保存等待执行任务的阻塞队列ArrayBlockingQueue
:是一个基于数组结构的有界队列LinkedBlockingQueue
:是一个基于链表结构的阻塞队列SynchronousQueue
:不存储元素的阻塞队列,每一个插入操作必须等到下一个线程调用移除操作,否则插入操作一直阻塞PriorityBlockingQueue
:一个具有优先级的无线阻塞队列threadFactory
:用来创建线程的工厂handler
:饱和策略,当线程池和队列都满了的时候,必须要采取一种策略处理新的任务,默认策略是AbortPolicy
,根据自己需求选择合适的饱和策略AbortPolicy
:直接抛出异常CallerRunsPolicy
:用调用者所在的线程来运行当前任务DiscardOldestPolicy
:丢弃队列里面最近的一个任务,并执行当前任务DiscardPolicy
:不处理,丢弃掉当然我们也可以通过实现
RejectedExecutionHandler
去自定义实现处理策略
入参不同,线程池的运行机制也不同,了解每个入参的含义由于我们更透传的理解线程池的实现原理
提交任务
线程池处理提交任务流程如下
处理流程:
- 如果核心线程数量未满,创建线程执行任务,否则添加到阻塞队列中
- 如果阻塞队列中未满,将任务存到队列里
- 如果阻塞队列满了,看线程池数量是否达到了线程池最大数量,如果没达到,创建线程执行任务
- 如果已经达到线程池最大数量,根据饱和策略进行处理
ThreadPoolExecutor
使用execute(Runnable command)
和submit(Runnable task)
向线程池中提交任务,在submit(Runnable task)
方法中调用了execute(Runnable command)
,所以我们只要了解execute(Runnable command)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取线程池状态,并且可以通过ctl获取到当前线程池数量及线程池状态
int c = ctl.get();
// 如果工作线程数小于核心线程数量,则创建一个新线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果不符合上面条件,当前线程处于运行状态并且写入阻塞队列成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 双重检查,再次获取线程状态,如果当前线程状态变为非运行状态,则从队列中移除任务,执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 检查工作线程数量是否为0
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//创建线程执行任务,如果添加失败则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
execute(Runnable command)
方法中我们比较关心的就是如何创建新的线程执行任务,就addWorker(command, true)
方法
workQueue.offer(command)
方法是用来向阻塞队列中添加任务的
reject(command)
方法会根据创建线程池时传入的饱和策略对任务进行处理,例如默认的AbortPolicy
,查看源码后知道就是直接抛了个RejectedExecutionException
异常,其他的饱和策略的源码也是特别简单
关于线程池状态与工作线程的数量是如何表示的
在ThreadPoolExecutor
中使用一个AtomicInteger
类型变量表示
/**
* ctl表示两个信息,一个是线程池的状态(高3位表示),一个是当前线程池的数量(低29位表示),这个跟我们前面 * 说过的读写锁的state变量是一样的,以一个变量记录两个信息,都是以利用int的32个字节,高十六位表述读,低十 * 六位表示写锁
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//低29位保存线程池数量
private static final int COUNT_BITS = Integer.SIZE - 3;
//线程池最大容量
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 运行状态存储在高3位
// 运行状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
addWorker(command, boolean)
创建工作线程,执行任务
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
// 线程池状态
int rs = runStateOf(c);
// 判断线程池状态,以及阻塞队列是否为空
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 获取线程工作线程数量
int wc = workerCountOf(c);
// 判断是否大于最大容量,以及根据传入的core判断是否大于核心线程数量还是最大线程数量
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 增加工作线程数量
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//如果线程池状态改变,则重试
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建Worker,内部创建了一个新的线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// 线程池状态判断
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 将创建的线程添加到线程池
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//执行任务,首先会执行Worker对象的firstTask
t.start();
workerStarted = true;
}
}
} finally {
//如果任务执行失败
if (! workerStarted)
//移除worker
addWorkerFailed(w);
}
return workerStarted;
}
关闭线程池
ThreadPoolExecutor
中关闭线程池使用shutdown()
和shutdownNow()
方法,原理都是通过遍历线程池中的线程,对线程进行中断
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
Executor框架
Executor
框架将任务的提交与任务的执行进行分离
Executors
提供了一系列工厂方法用于创先线程池,返回的线程池都实现了 ExecutorService
接口
工厂方法:
newFixedThreadPool
:用于创建固定数目线程的线程池newCachedThreadPool
:用于创建一个可缓存的线程池,调用execute将重用以前构造的线程,如果现有线程没有可用的,则创建一个新线 程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程newSingleThreadExecutor
:用于创建只有一个线程的线程池newScheduledThreadPool
:用于创建一个支持定时及周期性的任务执行的线程池
在阿里巴巴手册中强制要求禁止使用Executors
提供的工厂方法创建线程池
这个确实是一个很严重的问题,我们部门曾经就出现过使用FixedThreadPool
线程池,导致OOM,这是因为线程执行任务的时候被阻塞或耗时很长时间,导致阻塞队列一直在添加任务,直到内存被打满,报OOM
所以我们在使用线程池的时候要使用ThreadPoolExecutor
的构造函数去创建线程池,根据自己的任务类型来确定核心线程数和最大线程数,选择适合阻塞队列和阻塞队列的长度
合理配置线程池
合理的配置线程池需要分析一下任务的性质(使用ThreadPoolExecutor
创建线程池):
CPU密集型任务应配置竟可能小的线程,比如 cpu数量+1
IO密集型任务并不是一直在执行任务,应该配置尽可能多的线程,比如 cpu数量x2
可通过
Runtime.getRuntime().availableProcessors()
获取cpu数量执行的任务有调用外部接口比较费时的时候,这时cup空闲的时间就越长,可以将线程池数量设置大一些,这样cup空闲的时间就可以去执行别的任务
建议使用有界队列,可根据需要将长度设置大一些,防止OOM
参考:java并发编程的艺术
推荐阅读:
java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
Java 并发编程 | 线程池详解的更多相关文章
- Java同步之线程池详解
带着问题阅读 1.什么是池化,池化能带来什么好处 2.如何设计一个资源池 3.Java的线程池如何使用,Java提供了哪些内置线程池 4.线程池使用有哪些注意事项 池化技术 池化思想介绍 池化思想是将 ...
- Java多线程之线程池详解
前言 在认识线程池之前,我们需要使用线程就去创建一个线程,但是我们会发现有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因 ...
- Java并发编程——线程池的使用
在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统 ...
- Java并发编程——线程池
本文的目录大纲: 一.Java中的ThreadPoolExecutor类 二.深入剖析线程池实现原理 三.使用示例 四.如何合理配置线程池的大小 一.Java中的ThreadPoolExecutor类 ...
- Java并发编程--线程池
1.ThreadPoolExecutor类 java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,下面我们来看一下ThreadPoolExecuto ...
- JAVA 并发编程-线程池(七)
线程池的作用: 线程池作用就是限制系统中运行线程的数量. 依据系统的环境情况.能够自己主动或手动设置线程数量,达到运行的最佳效果:少了浪费了系统资源,多了造成系统拥挤效率不高.用线程池控制线程数量,其 ...
- java并发编程-线程池的使用
参考文章:http://www.cnblogs.com/dolphin0520/p/3932921.html 深入剖析线程池实现原理 将从下面几个方面讲解: 1.线程池状态 2.任务的执行 3.线程池 ...
- java 并发编程lock使用详解
浅谈Synchronized: synchronized是Java的一个关键字,也就是Java语言内置的特性,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,执行代码块时,其 ...
- 三、VIP课程:并发编程专题->01-并发编程之Executor线程池详解
01-并发编程之Executor线程池详解 线程:什么是线程&多线程 线程:线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系 ...
随机推荐
- 这道Java基础题真的有坑!我求求你,认真思考后再回答。
本文目录 一.题是什么题? 二.阿里Java开发规范. 2.1 正例代码. 2.2 反例代码. 三.层层揭秘,为什么发生异常了呢? 3.1 第一层:异常信息解读. 3.2 第二层:抛出异常的条件解读. ...
- C语言中表达n次方
C语言中表达n次方可以用pow函数. 函数原型:double pow(double x, double y) 功 能:计算x^y的值 返 回 值:计算结果 举例: double a; a = p ...
- 《Java知识应用》Java加密方式(Base64)详解
1. 说明 Base64加密方式:比较简单,加密快,对普通大众可以起到加密的作用.在程序员眼中和透明一样. Base64应用场景:图片转码(应用于邮件,img标签,http加密) 2. 案例 impo ...
- VS2019 开发Django(十)------JavaScript与Django的数据交互
导航:VS2019开发Django系列 这一篇介绍如何使用BootStrap Table这个组件来绑定渲染数据, 1)先来看一下BootStrap Table是怎么绑定数据的. 通过数据属性 给定da ...
- 后台管理tab栏滑动解决方案
后台管理系统中比较常见的布局是左边菜单栏,右边tab切换栏,但是一般的tab组件不包含tab页过多的切换问题的,所以需要个性化实现,本文的实现方案是滑动鼠标滚轮绑定tab达到切换的效果,先上一个动态图 ...
- Android 线性布局 计算器
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...
- Scrum Meeting - 第六周【Alpha阶段】
每日任务内容: 本次会议为第六次Scrum Meeting会议 本次会议项目经理召开时间为20:00,在北区男生宿舍楼召开,召开时长约15分钟,探讨了本周选课网站编写的后续工作. 小组成员 本周任务 ...
- Redis—数据备份与恢复
https://www.cnblogs.com/shizhengwen/p/9283973.html https://blog.csdn.net/w2393040183/article/details ...
- AI行业精选日报_人工智能(12·18)
百度Apollo升级自动驾驶平台,发布车路协同.智能车联两大平台 12 月 18 日消息,Apollo 发布了全球首个点到点城市自动驾驶开放能力.自动驾驶云.新一代智能交通解决方案.小度车载 2020 ...
- 深入理解 ZK集群中通过Processor保证数据一致性
入口 书接上篇博客中的ZK集群启动后完成数据的统一性恢复后,来到启动ZkServer的逻辑,接下来的重点工作就是启动不同角色的对应的不同的处理器Processor 如上图查看ZooKeeperServ ...