Java 线程池原理与使用
在java 中我们会一般要求创建线程必须使用线程池,因为这样可以避免资源消耗,通过重复利用已经创建的线程来降低线程创建和销毁所造成的消耗, 其次当任务到达时任务可以不用等到线程创建就立即执行,最后可以提高线程的管理性,管控线程,不让线程无限制创建.
线程池的创建
我们可以使用 ThreadPoolExecutor 创建线程池
这里我们不适用Executors 来创建线程池,是因为,Executors 使用默认参数不用自定义参数,
自己经常使用Executors提供的工厂方法创建线程池,所以忽略了线程池内部的实现,同时可以规避规避资源耗尽的风险.
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, handler);
- corePoolSize(线程池的基本大小): 当提交一个任务到线程池时,线程 池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务 也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。 如果调用了线程池的 prestartAllCoreThreads 方法,线程池会提前创建 并启动所有基本线程。
- runnableTaskQueue (任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队 列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必 须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态, 吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建 新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就 没什么效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创 建出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略): 当队列和线程池都满了,默认 AbortPolicy:直接抛出异常, 还可设置 CallerRunsPolicy:只用调用者所在线程来运行任务。 o DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前 任务。 o DiscardPolicy:不处理,丢弃掉。 o 当然也可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。如记录日志或持久化不能处理的任务。
向线程池提交任务
我们可以使用 execute 提交的任务,但是 execute 方法没有返回值,所以无法判 断任务是否被线程池执行成功。通过以下代码可知 execute 方法输入的任务是一 个 Runnable 类的实例。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
可以使用 submit 方法来提交任务,它会返回一个 future,那么我们可以 通过这个 future 来判断任务是否执行成功,通过 future 的 get 方法来获取返回 值, get 方法会阻塞住直到任务完成,而使用 get(long timeout, TimeUnit unit) 方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
2} finally {
// 关闭线程池
executor.shutdown();
}
线程池的关闭
调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池, 它们 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断 线程,所以无法响应中断的任务可能永远无法终止。
shutdown 和 shutdownNow 的区别
shutdownNow 首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或 暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状 态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。
通常调用 shutdown 来关闭线程池,表示任务执行完关闭,如果任务不一定要执行完, 则可以调用 shutdownNow。
线程池执行过程
提交一个任务到线程池,首先判断基本线程池是否已满,没有满不管线程池中有没有空闲线程都创建一个新线程来执行任务, 判断工作队列是否已满,没满则将新提交的任务存储在工作队列, 满了, 判断整个线程池是否已满, 没满, 则创建一个新的工作线程来 执行任务,满了,则交给饱和策略来处理这个任务.
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数小于基本线程数,则创建线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队
列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最
大允许的线程数量,
则创建一个线程执行任务。
else if (!addIfUnderMaximumPoolSize(command))
//抛出 RejectedExecutionException 异常
reject(command); // is shutdown or saturated
}
}
工作线程: 线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执 行完任务后,还会无限循环获取工作队列里的任务来执行。
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
合理的配置线程池
任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。 任务的优先级:高,中和低。 任务的执行时间:长,中和短。 任务的依赖性:是否依赖其他系统资源,如数据库连接。
- CPU 密集型任务, 配置尽 可能小的线程,如配置 Ncpu+1 个线程的线程池。
- IO 密集型任务则由于线程并不 是一直在执行任务,则配置尽可能多的线程,如 2*Ncpu。
- 混合型的任务,如果 可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两 个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞 吐率,如果这两个任务执行时间相差太大,则没必要进行分解。
- 优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以 让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到 队列里,那么优先级低的任务可能永远不能执行。
- 线程提交 SQL 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设 大一点,比如几千.
线程池的监控
通过继承线程池并重写线程池的 beforeExecute, afterExecute 和 terminated 方法,我们可以在任务执行前,执行后和线程池关 闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。
- go 语言的库文件放在哪里?如何通过nginx代理后还能正确获取远程地址
- 离线Tarjan算法-最近公共祖先问题
- Java文件上传下载实训
- 【网络编程系列】二:socket通信原理及实践
- textrank算法原理与提取关键词、自动提取摘要PYTHON
- 【网络编程系列】一:字节顺序的大端与小端表示法
- Linux下的make命令用法
- 增量数据丢失的原因分析(三)(r8笔记第91天)
- JS之浏览器对象BOM
- 超清晰的makefile解释、编写与示例
- 一个简单的sql审核案例 (r8笔记第90天)
- Linux wait() 和 waitpid()函数介绍
- #if和#ifdef的区别
- 一个MySQL优化案例的初步思路(r8笔记第87天)
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释