广告
返回顶部
首页 > 资讯 > 后端开发 > Python >超详细讲解Java线程池
  • 262
分享到

超详细讲解Java线程池

2024-04-02 19:04:59 262人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

目录池化技术池化思想介绍池化技术的应用如何设计一个线程池Java线程池解析ThreadPoolExecutor使用介绍内置线程池使用ThreadPoolExecutor解析整体设计线

带着问题阅读

1、什么是池化,池化能带来什么好处

2、如何设计一个资源池

3、Java的线程池如何使用,Java提供了哪些内置线程池

4、线程池使用有哪些注意事项

池化技术

池化思想介绍

池化思想是将重量级资源预先准备好,在使用时可重复使用这些预先准备好的资源。

池化思想的核心概念有:

  • 资源创建/销毁开销大
  • 提前创建,集中管理
  • 重复利用,资源可回收

例如大街上的共享单车,用户扫码开,使用完后归还到停放点,下一个用户可以继续使用,共享单车由厂商统一管理,为用户节省了购买单车的开销。

池化技术的应用

常见的池化技术应用有:资源池、连接池、线程池等。

  • 资源池

在各种电商平台大促活动时,平台需要支撑平时几十倍的流量,因此各大平台在需要提前准备大量服务器进行扩容,在活动完毕以后,扩容的服务器资源又白白浪费。将计算资源池化,在业务高峰前进行分配,高峰结束后提供给其他业务或用户使用,即可节省大量消耗,资源池化也是云计算的核心技术之一。

  • 连接池

网络连接的建立和释放也是一个开销较大的过程,提前在服务器之间建立好连接,在需要使用的时候从连接池中获取,使用完毕后归还连接池,以供其他请求使用,以此可节省掉大量的网络连接时间,如数据库连接池、HttpClient连接池。

  • 线程池

线程的建立销毁都涉及到内核态切换,提前创建若干数量的线程提供给客户端复用,可节约大量的CPU消耗以便处理业务逻辑。线程池也是接下来重点要讲的内容。

如何设计一个线程池

设计一个线程池,至少需要提供的核心能力有:

  • 线程池容器:用于容纳初始化时预先创建的线程。
  • 线程状态管理:管理池内线程的生命周期,记录每个线程当前的可服务状态。
  • 线程请求管理:对调用端提供获取和归还线程的接口。
  • 线程耗尽策略:提供策略以处理线程耗尽问题,如拒绝服务、扩容线程池、排队等待等。

基于以上角度,我们来分析Java是如何设计线程池功能的。

Java线程池解析

ThreadPoolExecutor使用介绍

大象装冰箱总共分几步


// 1.创建线程池
ThreadPoolExecutor threadPool = 
    new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
// 2.提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("task running");
    }
}});
// 3.关闭线程池
threadPool.shutDown();

Java通过ThreadPoolExecutor提供线程池的实现,如示例代码,初始化一个容量为1的线程池、然后提交任务、最后关闭线程池。

ThreadPoolExecutor的核心方法主要有

  • 构造函数:ThreadPoolExecutor提供了多个构造函数,以下对基础构造函数进行说明。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:线程池的核心线程数。池内线程数小于corePoolSize时,线程池会创建新线程执行任务。
  • maximumPoolSize:线程池的最大线程数。池内线程数大于corePoolSizeworkQueue任务等待队列已满时,线程池会创建新线程执行队列中的任务,直到线程数达到maximumPoolSize为止。
  • keepAliveTime:非核心线程的存活时长。池内超过corePoolSize数量的线程可存活的时长。
  • unit:非核心线程存活时长单位。与keepAliveTime取值配合,如示例代码表示1分钟。
  • workQueue:任务提交队列。当无空闲核心线程时,存储待执行任务。

类型 作用
ArrayBlockingQueue 数组结构的有界阻塞队列
LinkedBlockingQueue 链表结构的阻塞队列,可设定是否有界
SynchronousQueue 不存储元素的阻塞队列,直接将任务提交给线程池执行
PriorityBlockingQueue 支持优先级的无界阻塞队列
DelayQueue 支持延时执行的无界阻塞队列

  • threadFactory:线程工厂。用于创建线程对象。
  • handler:拒绝策略。线程池线程数量达到maximumPoolSizeworkQueue已满时的处理策略。

类型 作用
AbortPolicy 拒绝并抛出异常。默认
CallerRunsPolicy 由提交任务的线程执行任务
DiscardOldestPolicy 抛弃队列头部任务
DiscardPolicy 抛弃该任务

  • 执行函数:executesubmit,主要分别用于执行RunnableCallable

// 提交Runnable
void execute(Runnable command);

// 提交Callable并返回Future
<T> Future<T> submit(Callable<T> task);

// 提交Runnable,执行结束后Future.get会返回result
<T> Future<T> submit(Runnable task, T result);

// 提交Runnable,执行结束后Future.get会返回null
Future<?> submit(Runnable task);
  • 停止函数:shutDownshutDownNow

// 不再接收新任务,等待剩余任务执行完毕后停止线程池
void shutdown();

// 不再接收新任务,并尝试中断执行中的任务,返回还在等待队列中的任务列表
List<Runnable> shutdownNow();

内置线程池使用

To be useful across a wide range of contexts, this class provieds many adjustable parameters and extensibility hooks. However, programmers are urged to use the more convenient {@link Executors} factory methods {@link Executors#newCachedThreadPool} (unbounded thread poll, with automatic thread reclamation), {@link Executors#newFixedThreadPool} (fixed size thread pool) and {@link Executors#newSingleThreadExecutor}(single background thread), that preconfigure settings for the most common usage scenariOS.

由于ThreadPoolExecutor参数复杂,Java提供了三种内置线程池newCachedThreadPoolnewFixedThreadPoolnewSingleThreadExecutor应对大多数场景。

  • Executors.newCachedThreadPool()无界线程池,核心线程池大小为0,最大为Integer.MAX_VALUE,因此严格来讲并不算无界。采用SynchronousQueueworkQueue,意味着任务不会被阻塞保存在队列,而是直接递交到线程池,如线程池无可用线程,则创建新线程执行。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
                                  new SynchronousQueue<Runnable>());
}
  • Executors.newFixedThreadPool(int nThreads)固定大小线程池,其中coreSizemaxSize相等,且过期时间为0,表示经过一定数量任务提交后,线程池将始终维持在nThreads数量大小,不会新增也不会回收线程。

public static ExecutorService new FixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads nThreads, 0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • Executors.newSingleThreadExecutor()单线程池,参数与fixedThreadPool类似,只是将数量限制在1,单线程池主要避免重复创建销毁线程对象,也可用于串行化执行任务。不同与其他线程池,单线程池采用FinallizableDelegatedExecutorServiceThreadPoolExecutor对象进行包装,感兴趣的同学可以看下源码,其方法实现仅仅是对被包装对象方法的直接调用。包装对象主要用于避免用户将线程池强制转换为ThreadPoolExecutor来修改线程池大小。

public static ExecutorService newSingleThreadExecutor() {
    return new FinallizableDelegatedExecutorService(
      (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, 
                                new LinkedBlockQueue<Runnable>()))
    );
}

ThreadPoolExecutor解析

整体设计

ThreadPoolExecutor基于ExecutorService接口实现提交任务,未采取常规资源池获取/归还资源的形式,整个线程池和线程的生命周期都由ThreadPoolExecutor进行管理,线程对象不对外暴露;ThreadPoolExecutor的任务管理机制类似于生产者消费者模型,其内部维护一个任务队列和消费者,一般情况下,任务被提交到队列中,消费线程从队列中拉取任务并将其执行。

线程池生命周期


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static int runStateOf(int c)     { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c)  { return c & CAPACITY; }  //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; }   //通过状态和线程数生成ctl

TreadPoolExecutor通过ctl维护线程池的状态和线程数量,其中高3位存储运行状态,低29位存储线程数量。

线程池设定了RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED五种状态,其转移图如下:

在这5种状态中,只有RUNNING时线程池可接收新任务,其余4种状态在调用shutDownshutDownNow后触发转换,且在这4种状态时,线程池均不再接收新任务。

任务管理解析


// 用于存放提交任务的队列
private final BlockingQueue<Runnable> workQueue;

// 用于保存池内的工作线程,Java将Thread包装成Worker存储
private final HashSet<Worder> workers = new HashSet<Worker>();

ThreadPoolExecutor主要通过workQueueworkers两个字段用于管理和执行任务。

线程池任务执行流程如图,结合ThreadPoolExecutor.execute源码,对任务执行流程进行说明:

  • 当任务提交到线程池时,如果当前线程数量小于核心线程数,则会将为该任务直接创建一个worker并将任务交由worker执行。

if (workerCountOf(c) < corePoolSize) {
    // 创建新worker执行任务,true表示核心线程
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
  • 当已经达到核心线程数后,任务会提交到队列保存;

// 放入workQueue队列
if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    // 这里采用double check再次检测线程池状态
    if (! isRunning(recheck) && remove(command))
        reject(command);
    // 避免加入队列后,所有worker都已被回收无可用线程
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
  • 如果队列已满,则依据最大线程数量创建新worker执行。如果新增worker失败,则依据设定策略拒绝任务。

// 接上,放入队列失败
// 添加新worker执行任务,false表示非核心线程
else if (!addWorker(command, false))
    // 如添加失败,执行拒绝策略
    reject(command);

woker对象

ThreadPoolExecutor没有直接使用Thread记录线程,而是定义了worker用于包装线程对象。


private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    ...
    final Thread thread;
    
    Runnable firstTask;
    
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    // worker对象被创建后就会执行
    public void run() {
        runWorker(this);
    }
}

worker对象通过addWorker方法创建,一般会为其指定一个初始任务firstTask,当worker执行完毕以后,worker会从阻塞队列中读取任务,如果没有任务,则该worker会陷入阻塞状态给出worker的核心逻辑代码:


private boolean addWorker(Runnable firstTask, boolean core) {
    ...
    // 指定firstTask,可能为null
    w = new Worker(firstTask);
    ...
    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
        if (t.isAlive()) // precheck that t is startable
            throw new IllegalThreadStateException();
        workers.add(w);
        workerAdded = true;
    }
    ...
    // 执行新添加的worker
    if (workerAdded) {
        t.start();
        workerStarted = true;
    }
}


final void runWorker(Worker w) {
    // 等待workQueue的任务
    while (task != null || (task = getTask()) != null) {
    	...
    }
}

private Runnable getTask() {
    ...
    for (;;) {
        ...
        // 如果是普通工作线程,则根据线程存活时间读取阻塞队列
        // 如果是核心工作线程,则直接陷入阻塞状态,等待workQueue获取任务
        Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
        ...
    }
}

如下图,任务提交后触发addWorker创建worker对象,该对象执行任务完毕后,则循环获取队列中任务等待执行。

Java线程池实践建议

不建议使用Exectuors

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。《阿里巴巴开发手册》

虽然Java推荐开发者直接使用Executors提供的线程池,但实际开发中通常不使用。主要考虑问题有:

  • 潜在的OOM问题

CachedThreadPool将最大数量设置为Integer.MAX_VALUE,如果一直提交任务,可能造成Thread对象过多引起OOMFixedThreadPoolSingleThreadPoo的队列LinkedBlockingQueue无容量限制,阻塞任务过多也可能造成OOM

  • 线程问题定位不便

由于未指定ThreadFactory,线程名称默认为pool-poolNumber-thread-thredNumber,线程出现问题后不便定位具体线程池。

  • 线程池分散

通常在完善的项目中,由于线程是重量资源,因此线程池由统一模块管理,重复创建线程池容易造成资源分散,难以管理。

线程池大小设置

通常按照IO繁忙型和CPU繁忙型任务分别采用以下两个普遍公式。

在理论场景中,如一个任务IO耗时40ms,CPU耗时10ms,那么在IO处理期间,CPU是空闲的,此时还可以处理4个任务(40/10),因此理论上可以按照IO和CPU的时间消耗比设定线程池大小。

《JAVA并发编程实践》中还考虑数量乘以目标CPU的利用率

在实际场景中,我们通常无法准确测算IO和CPU的耗时占比,并且随着流量变化,任务的耗时占比也不能固定。因此可根据业务需求,开设线程池运维接口,根据线上指标动态调整线程池参数。

推荐参考第二篇美团线程池应用

线程池监控

ThreadPoolExecutor提供以下方法监控线程池:

  • getTaskCount() 返回被调度过的任务数量
  • getCompletedTaskCount() 返回完成的任务数量
  • getPoolSize() 返回当前线程池线程数量
  • getActiveCount() 返回活跃线程数量
  • getQueue()获取队列,一般用于监控阻塞任务数量和队列空间大小

到此这篇关于超详细讲解Java线程池的文章就介绍到这了,更多相关Java 线程池内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 超详细讲解Java线程池

本文链接: https://www.lsjlt.com/news/134496.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 超详细讲解Java线程池
    目录池化技术池化思想介绍池化技术的应用如何设计一个线程池Java线程池解析ThreadPoolExecutor使用介绍内置线程池使用ThreadPoolExecutor解析整体设计线...
    99+
    2022-11-12
  • Java线程池详细解读
    目录1、线程池1.1 线程池概念1.2 线程池的实现2、StringBuffer类面试题:请解释String、StringBuffer、StringBuilder的区别?3、...
    99+
    2022-11-12
  • Java超详细讲解多线程中的Process与Thread
    目录进程和线程的关系操作系统是如何管理进程的并行和并发创建线程的方法串行执行和并发执行Thread中的一次额重要方法中断线程线程等待线程休眠(sleep)进程和线程的关系 ⭐在操作系...
    99+
    2022-11-13
  • JAVA多线程详解(超详细)
    目录 一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态 二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口(不常用) 三、线程常用方法1、线程的状态2、线程常用方法 四...
    99+
    2023-08-19
    java jvm 开发语言
  • java反射超详细讲解
    目录Java反射超详解✌1.反射基础1.1Class类1.2类加载2.反射的使用2.1Class对象的获取2.2Constructor类及其用法2.4Method类及其用...
    99+
    2022-11-12
  • 超详细讲解Java异常
    目录一、Java异常架构与异常关键字Java异常简介Java异常架构1、Throwable2、Error(错误)3、Exception(异常)4、受检异常与非受检异常Java异常关键...
    99+
    2022-11-12
  • Python进程间通讯与进程池超详细讲解
    目录进程间通讯队列Queue管道Pipe进程池Pool在《多进程并发与同步》中介绍了进程创建与信息共享,除此之外python还提供了更方便的进程间通讯方式。 进程间通讯 multip...
    99+
    2022-12-23
    Python进程间通讯 Python进程池
  • Java超详细讲解hashCode方法
    目录1、介绍一下hashCode方法2、为什么需要hashCode方法?3、hashCode(),equals()两种方法是什么关系?4、为什么重写 equals 方法必须重...
    99+
    2022-11-13
  • Java超详细透彻讲解static
    目录1. 引入2. 理解3. 使用3.1 使用范围3.2 static修饰属性3.2.1 设计思想3.2.2 分类3.2.3 注意3.2.4 举例3.2.5 类变量内存解析3.3 s...
    99+
    2022-11-13
  • Java 超详细讲解字符流
    目录一、字符流的由来二、编码表字符集:Unicode字符集:UTF-8编码规则:三、字符串中的编码解码问题编码方法(IDEA):解码方法(IDEA):四、字符流的编码解码问题四、字符...
    99+
    2022-11-13
  • java多线程(超详细)
    1 - 线程 1.1 - 进程 进程就是正在运行中的程序(进程是驻留在内存中的) 是系统执行资源分配和调度的独立单位 每一进程都有属于自己的存储空间和系统资源 注意:进程A和进程B的内存独立不共享。 1.2 - 线程 线程...
    99+
    2023-08-31
    jvm java intellij idea 开发语言
  • SpringCloudNetflixRibbon超详细讲解
    目录一、Ribbon简介1、什么是Ribbon2、Ribbon能干什么二、使用Ribbon1、客户端导入依赖2、application.yml配置3、Controller配置4、Co...
    99+
    2022-11-13
    SpringCloud Netflix Ribbon SpringCloud Ribbon
  • AndroidLayerDrawable超详细讲解
    目录1. 前言2. 实例1. 前言 Android LayerDrawble 包含一个Drawable数组,系统将会按照这些Drawable对象的数组顺序来绘制他们,索引最大的 Dr...
    99+
    2022-11-13
    Android LayerDrawable Android LayerDrawable原理
  • AndroidSwipeRefreshLayout超详细讲解
    目录1. 控件说明2. API介绍3. 使用方法3.1 布局文件3.2 界面代码4. 注意事项1. 控件说明 SwipeRefreshLayout是google官方推荐使用的下拉刷新...
    99+
    2022-11-13
    Android SwipeRefreshLayout Android SwipeRefreshLayout控件
  • C语言超详细讲解线性表
    目录1. 顺序表1.1 管理结点1.2 顺序表的插入1.3 顺序表的删除1.4 顺序表的扩容2. 链表2.1 定义2.2 头部插入2.3 尾部插入2.4 任意位置插入2.5 任意位置...
    99+
    2022-11-13
  • Java 泛型超详细入门讲解
    目录1、什么是泛型2、泛型是怎么编译的泛型的编译机制:擦除机制1、什么是泛型 泛型其实就是将类型作为参数传递,泛型允许程序员在编写代码时使用一些以后才指定的类型 ,在实例化该类时将想...
    99+
    2022-11-13
  • Java 超详细讲解SpringMVC拦截器
    目录拦截器(interceptor)的作用拦截器和过滤器区别拦截器快速入门多拦截器操作拦截器方法说明本章小结拦截器(interceptor)的作用 Spring MVC 的 拦截器 ...
    99+
    2022-11-13
  • Java超详细讲解类的继承
    目录写在前面1.子类的创建1.1子类的创建方法1.2调用父类中特定的构造方法2.在子类中访问父类成员3.覆盖3.1覆盖父类中的方法3.2用父类的对象访问子类的成员4.不可被继承的成员...
    99+
    2022-11-13
  • Java超详细透彻讲解接口
    目录一、引入二、理解三、使用四、应用-代理模式(Proxy)1. 应用场景2. 分类3. 代码演示五、接口和抽象类之间的对比六、经典题目(排错)一、引入 一方面,有时必须从几个类中派...
    99+
    2022-11-13
  • Java贪心算法超详细讲解
    目录什么是贪心算法通过场景理解算法问题分析总结什么是贪心算法 在分析和求解某个问题时,在每一步的计算选择上都是最优的或者最好的,通过这种方式期望最终的计算的结果也是最优的。也就是说,...
    99+
    2022-11-13
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作