为什么需要线程池

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种不同的线程池

  1. newFixedThreadPooll(int nThreads)创建一个指定工作线程数量的线程池

    coolPoolSize = maximumPoolSize = nThreads

    使用的是无界的工作队列LinkBlockingQueue,每提交一个任务就创建一个工作线程,如果工作线程数量达到coolPoolSize,则将提交的任务存入到队列中等待。

    maximumPoolSize和keepAliveTime无效。

  2. newCachedThreadPooll()创建一个可缓存的线程池,用来处理大量短时间工作任务的线程池。特点是:

    • 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。工作线程的创建数量有限制为Interger. MAX_VALUE。
    • 如果工作线程空闲超过1分钟,将自动终止并移出缓存。长时间闲置时,这种线程池,不会消耗什么资源。
    • 其内部使用SynchronousQueue作为工作队列
  3. newSingleThreadExecutor()创建一个单线程化的Executor

    只创建唯一的工作者线程,如果这个线程异常结束会有另一个取代它。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间只有一个线程在工作。

  4. newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)。

    创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

  5. 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 + 平均等待时间/平均工作时间)

线程池使用的注意事项

  1. 避免任务堆积。工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现OOM。
  2. 避免过度扩展线程。通常在处理大量短时任务时,使用可缓存的线程池,但很难明确设置线程数目。
  3. 避免线程泄漏。往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放,当线程数目不断增长时造成溢出。
  4. 避免死锁
  5. 避免在使用线程池时操作ThreadLocal。因为ThreadLocalMap中废弃项目的回收依赖于显式地触发,否则就要等待线程结束,内存自动回收弱引用,进而回收相应ThreadLocalMap,但worker线程往往是不会退出的,这就容易出现OOM。
  6. Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,所以建议使用有界队列

参考

《Java并发实战》

《Java核心技术36讲》杨晓峰

深入理解Java多线程——线程池的更多相关文章

  1. 深入理解Java之线程池(爱奇艺面试)

    爱奇艺的面试官问 (1) 线程池是如何关闭的 (2) 如何确定线程池的数量 一.线程池销毁,停止线程池 ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown() ...

  2. 深入理解Java之线程池

    原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...

  3. [转]深入理解Java之线程池

    原文链接 原文出处: 海 子 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这 ...

  4. java多线程——线程池源码分析(一)

    本文首发于cdream的个人博客,点击获得更好的阅读体验! 欢迎转载,转载请注明出处. 通常应用多线程技术时,我们并不会直接创建一个线程,因为系统启动一个新线程的成本是比较高的,涉及与操作系统的交互, ...

  5. [Java多线程]-线程池的基本使用和部分源码解析(创建,执行原理)

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 多线 ...

  6. 跟我学Java多线程——线程池与堵塞队列

    前言 上一篇文章中我们将ThreadPoolExecutor进行了深入的学习和介绍,实际上我们在项目中应用的时候非常少有直接应用ThreadPoolExecutor来创建线程池的.在jdk的api中有 ...

  7. 深入理解Java之线程池(网络笔记)

    原文链接:http://www.cnblogs.com/dolphin0520/p/3932921.html 附加:http://www.cnblogs.com/wxd0108/p/5479442.h ...

  8. java多线程--线程池的使用

    程序启动一个新线程的成本是很高的,因为涉及到要和操作系统进行交互,而使用线程池可以很好的提高性能,尤其是程序中当需要创建大量生存期很短的线程时,应该优先考虑使用线程池. 线程池的每一个线程执行完毕后, ...

  9. java多线程-线程池

    线程池(Thread Pool)对于限制应用程序中同一时刻运行的线程数很有用.因为每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等等. 我们可以把并发执行的任务传递给一个线程池, ...

随机推荐

  1. HTML的一些标签以及表单

    HTML的一些标签以及表单 图片标签 属性 说明 src 图像的路径 alt 图像不能显示时的替换文字 title 鼠标悬停时显示的内容 border 设置图像边框的宽度 align 对齐方式 相对路 ...

  2. 多表联合查询 - 基于注解SQL

    作者:汤圆 个人博客:javalover.cc 前言 背景:Spring Boot + MybatisPlus 用MybatisPlus就是为了不写SQL,用起来方便: 但是如果需要多表联合查询,还是 ...

  3. 【MybatisPlus】数据库的datetime类型字段为空的时候,报错空指针?

    一.发现经历 事情是这样的,我今天本来要演示系统,就去前端同学的页面上点一点.不小心点到了其他同事编写的服务,然后界面就报错了.这给我吓得,这还能演示吗这.然后,我就去服务器查看了一下日志,发现了如下 ...

  4. Go语言的函数05---匿名函数

    package main import ( "fmt" "time" ) //延时执行一个匿名函数 func main071() { fmt.Println(& ...

  5. Jittor框架API

    Jittor框架API 这里是Jittor主模块的API文档,可以通过import jittor来获取该模块. classjittor.ExitHooks exc_handler(exc_type,  ...

  6. BEP 7:CUDA外部内存管理插件(下)

    BEP 7:CUDA外部内存管理插件(下) Numba依赖 向库中添加EMM插件的实现自然会使Numba成为库的依赖项,而以前可能没有.为了使依赖关系可选,如果需要的话,可以有条件地实例化并注册EMM ...

  7. 适用于Windows和Linux的Yolo-v3和Yolo-v2(上)

    适用于Windows和Linux的Yolo-v3和Yolo-v2(上) https://github.com/eric-erki/darknetAB (用于对象检测的神经网络)-Tensor Core ...

  8. Java 将PPT幻灯片转为HTML

    本文以Java程序代码为例展示如何通过格式转换的方式将PPT幻灯片文档转为HTML文件.这里的PPT幻灯片可以是.ppt/.pptx/.pps/.ppsx/.potx等格式. 代码实现思路:[加载PP ...

  9. 【NX二次开发】Block UI 对象颜色选择器

    属性说明 常规         类型 描述     BlockID     String 控件ID     Enable     Logical 是否可操作     Group     Logical ...

  10. 【NX二次开发】Block UI 表达式

    属性说明 常规         类型 描述     BlockID     String 控件ID     Enable     Logical 是否可操作     Group     Logical ...