深入理解Java多线程——线程池
为什么需要线程池
new Thread()不是创建一个对象那么简单,需要调用操作系统内核的API,然后操作系统要为线程分配一系列的资源,这个成本就很高。所以线程是一个重量级的对象,应该避免频繁创建和销毁。而应对方案就是线程池。
定义
线程池,除了池的功能外,还提供了更全面的线程管理、任务提交等方法。带来的好处是:
- 降低资源消耗
- 提高任务响应速度
- 提高线程可管理性
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- workQueue,工作队列负责存储用户提交的各个任务,下面会详细介绍。
- corePoolSize,核心线程数,可以理解为长期驻留的线程数目(除非设置了allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如newFixedThreadPool会将其设置为nThreads,而对于newCachedThreadPool则是为0。
- maximumPoolSize,线程不够时能够创建的最大线程数。对于newFixedThreadPool,就是nThreads,因为其要求是固定大小,而newCachedThreadPool则是Integer.MAX_VALUE 。
- keepAliveTime和TimeUnit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。
- threadFactory,自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
- handler,自定义任务的拒绝策略。
- 内部的“线程池”,是保存工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。线程池的工作线程被抽象为静态内部类Worker,基于AQS实现。
private final HashSet<Worker> workers = new HashSet<>();
- ctl变量是一个非常有意思的设计,它被赋予了双重角色,通过高低位的不同,既表示线程池状态,又表示工作线程数目,这是一个典型的高效优化。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 真正决定了工作线程数的理论上限
private static fnal int COUNT_BITS = Integer.SIZE - 3;
private static fnal int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 线程池状态,存储在数字的高位
private static fnal int RUNNING = -1 << COUNT_BITS;
…
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
private static int workerCountOf(int c) { return c & COUNT_MASK; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
工作队列workQueue
即各种BlockingQueue
- 直接提交队列。synchronousQueue,是特殊的BlockingQueue, 容量为0,没执行一个插入就会阻塞,需要执行删除才能唤醒。
- 有界队列。ArrayBlockingQueue,来了新任务时,pool创建工作线程,直到线程数达到corePoolSize,此时将任务加入有界队列。如果队列满了,则继续创建线程,直到maximimPoolSize,此时执行拒绝策略。
- 无界队列。LinkBlockingQueue,无界,注意OOM问题,提倡使用有界队列。
不同的线程池
JUC的Executors目前提供了5种不同的线程池
newFixedThreadPooll(int nThreads)创建一个指定工作线程数量的线程池。
coolPoolSize = maximumPoolSize = nThreads
使用的是无界的工作队列LinkBlockingQueue,每提交一个任务就创建一个工作线程,如果工作线程数量达到coolPoolSize,则将提交的任务存入到队列中等待。
maximumPoolSize和keepAliveTime无效。newCachedThreadPooll()创建一个可缓存的线程池,用来处理大量短时间工作任务的线程池。特点是:
- 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。工作线程的创建数量有限制为Interger. MAX_VALUE。
- 如果工作线程空闲超过1分钟,将自动终止并移出缓存。长时间闲置时,这种线程池,不会消耗什么资源。
- 其内部使用SynchronousQueue作为工作队列
newSingleThreadExecutor()创建一个单线程化的Executor。
只创建唯一的工作者线程,如果这个线程异常结束会有另一个取代它。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间只有一个线程在工作。newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)。
创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。newWorkStealingPool(int parallelism),JDK1.8引入。
内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
work-stealing pool的实现
Executor

Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,使开发者不被太多线程创建、调度等不相关细节所打扰。
void execute(Runnable command);
ExecutorService则更加完善,不仅提供service的管理功能,比如shutdown等方法,也提供了更加全面的提交任务机制,如返回Future而不是void的submit方法。
<T> Future<T> submit(Callable<T> task);
线程池的工作原理
线程池是一种生产者-消费者模式,而不是经典池化资源的获取/释放模式。
为什么线程池没有采用一般意义上池化资源的设计方法呢?因为找不到似execute(Runnable target)这种方法执行业务逻辑。
//采⽤⼀般意义上池化资源的设计⽅法
class ThreadPool{
// 获取空闲线程
Thread acquire() {
}
// 释放线程
void release(Thread t){
}
}
//期望的使⽤
ThreadPool pool;
Thread T1=pool.acquire();
//传⼊Runnable对象
T1.execute(()->{
//具体业务逻辑
......
});
所以,采用了生产者消费者模式,线程池的使用方是生产者,线程池本身是消费者,产品则是业务任务work,work被放入工作队列workQueue。
可以把线程池类比为一个项目组,而线程就是项目组的成员。

线程池生命周期

线程池增长策略
任务通过execute(Runable)添加到pool
public void execute(Runnable command) {
…
int c = ctl.get();
// 检查工作线程数目,低于corePoolSize则添加Worker
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// isRunning就是检查线程池是否被shutdown
// 工作队列可能是有界的,ofer是比较友好的入队方式
if (isRunning(c) && workQueue.ofer(command)) {
int recheck = ctl.get();
// 再次进行防御性检查
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 尝试添加一个worker,如果失败以为着已经饱和或者被shutdown了
else if (!addWorker(command, false))
reject(command);
}
线程池大小的设置
如果我们的任务主要是进行计算,通常建议按照CPU核的数目N或者N+1。
如果是需要较多等待的任务,例如I/O操作比较多,可以参考Brain Goetz推荐的计算方法:线程数 = CPU核数 × (1 + 平均等待时间/平均工作时间)
线程池使用的注意事项
- 避免任务堆积。工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现OOM。
- 避免过度扩展线程。通常在处理大量短时任务时,使用可缓存的线程池,但很难明确设置线程数目。
- 避免线程泄漏。往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放,当线程数目不断增长时造成溢出。
- 避免死锁
- 避免在使用线程池时操作ThreadLocal。因为ThreadLocalMap中废弃项目的回收依赖于显式地触发,否则就要等待线程结束,内存自动回收弱引用,进而回收相应ThreadLocalMap,但worker线程往往是不会退出的,这就容易出现OOM。
- Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,所以建议使用有界队列
参考
《Java并发实战》
《Java核心技术36讲》杨晓峰
深入理解Java多线程——线程池的更多相关文章
- 深入理解Java之线程池(爱奇艺面试)
爱奇艺的面试官问 (1) 线程池是如何关闭的 (2) 如何确定线程池的数量 一.线程池销毁,停止线程池 ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown() ...
- 深入理解Java之线程池
原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...
- [转]深入理解Java之线程池
原文链接 原文出处: 海 子 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这 ...
- java多线程——线程池源码分析(一)
本文首发于cdream的个人博客,点击获得更好的阅读体验! 欢迎转载,转载请注明出处. 通常应用多线程技术时,我们并不会直接创建一个线程,因为系统启动一个新线程的成本是比较高的,涉及与操作系统的交互, ...
- [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...
- 跟我学Java多线程——线程池与堵塞队列
前言 上一篇文章中我们将ThreadPoolExecutor进行了深入的学习和介绍,实际上我们在项目中应用的时候非常少有直接应用ThreadPoolExecutor来创建线程池的.在jdk的api中有 ...
- 深入理解Java之线程池(网络笔记)
原文链接:http://www.cnblogs.com/dolphin0520/p/3932921.html 附加:http://www.cnblogs.com/wxd0108/p/5479442.h ...
- java多线程--线程池的使用
程序启动一个新线程的成本是很高的,因为涉及到要和操作系统进行交互,而使用线程池可以很好的提高性能,尤其是程序中当需要创建大量生存期很短的线程时,应该优先考虑使用线程池. 线程池的每一个线程执行完毕后, ...
- java多线程-线程池
线程池(Thread Pool)对于限制应用程序中同一时刻运行的线程数很有用.因为每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等等. 我们可以把并发执行的任务传递给一个线程池, ...
随机推荐
- WebConfig配置,添加静态资源,外部可以直接访问地址
此配置是处理springboot拦截静态文件的 代码如下: @Configuration public class WebMvcConfig implements WebMvcConfigurer { ...
- SpringBoot 实现整合log4j2日志
关于日志级别 共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF. ...
- Step By Step(C调用Lua)
Step By Step(C调用Lua) 1. 基础: Lua的一项重要用途就是作为一种配置语言.现在从一个简单的示例开始吧. --这里是用Lua代码定义的窗口大小的配置信息 wid ...
- 面部表情视频中进行远程心率测量:ICCV2019论文解析
面部表情视频中进行远程心率测量:ICCV2019论文解析 Remote Heart Rate Measurement from Highly Compressed Facial Videos: an ...
- C ++基本输入/输出
C ++基本输入/输出 本文将学习如何使用cin对象从用户那里获取输入,并使用cout对象在示例的帮助下向用户显示输出. C ++输出 在C ++中,cout将格式化的输出发送到标准输出设备,例如屏幕 ...
- 激光雷达Lidar与毫米波雷达Radar:自动驾驶的利弊
激光雷达Lidar与毫米波雷达Radar:自动驾驶的利弊 Lidar vs Radar: pros and cons for autonomous driving 新型无人驾驶汽车的数量在缓慢增加,各 ...
- MEMS传感器作为变革的驱动力
MEMS sensors as drivers for change 物联网(IoT)正在改变与周围世界互动的方式.每个人,每件事,都是相互联系的,很快就会相互联系.微机电系统(MEMS)设备和传感器 ...
- thymeleaf+Springboot实现自定义标签
在项目开发中,有一些组件不能满足我们快速开发的要求,我们需要封装一些组件来更加的便利我们.比如,我们可以封装一个下拉框组件,只要开发人员只有引用这个组件的标签,就能出现效果,而不用再去请求url,渲染 ...
- redis 系列,这里转发别人博客, 和常用命令
https://blog.csdn.net/qq_35433716/category_7944890.html 常用命令: https://www.cnblogs.com/mznsndy/p/1395 ...
- 理解Spring:AOP的原理及手动实现
引入 到目前为止,我们已经完成了简易的IOC和DI的功能,虽然相比如Spring来说肯定是非常简陋的,但是毕竟我们是为了理解原理的,也没必要一定要做一个和Spring一样的东西.到了现在并不能让我们松 ...