Java线程池工作原理浅析

news2024/11/27 0:43:07

为什么要用线程池?

1、线程属于稀缺资源,它的创建会消耗大量系统资源

2、线程频繁地销毁,会频繁地触发GC机制,使系统性能降低

3、多线程并发执行缺乏统一的管理与监控

线程池的使用

线程池的创建使用可通过Executors类来完成,它提供了创建线程池的常用方法。

·newFixedThreadPool

·newSingleThreadExecutor

·newCachedThreadPool

·newScheduledThreadPool

用例

public static void main(String[] args) {
        test();
    }

    public static void test() {
        ExecutorService service = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 30; i++) {
//            System.out.println("task name:addition_isCorrect");
            service.execute(new MyRunnable("executor-" + i));
        }

    }

    public static class MyRunnable implements Runnable {

        String name;

        public MyRunnable(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(3000);
                System.out.println( );
                System.out.println("time = " + System.currentTimeMillis() + "--task name:" + name + "---thread name:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
time = 1711718334177--task name:executor-1---thread name:pool-1-thread-2

time = 1711718334177--task name:executor-0---thread name:pool-1-thread-1
time = 1711718334177--task name:executor-2---thread name:pool-1-thread-3



time = 1711718337191--task name:executor-5---thread name:pool-1-thread-3
time = 1711718337191--task name:executor-4---thread name:pool-1-thread-1
time = 1711718337191--task name:executor-3---thread name:pool-1-thread-2


time = 1711718340193--task name:executor-6---thread name:pool-1-thread-3
time = 1711718340193--task name:executor-7---thread name:pool-1-thread-1

time = 1711718340201--task name:executor-8---thread name:pool-1-thread-2

time = 1711718343204--task name:executor-10---thread name:pool-1-thread-1


time = 1711718343204--task name:executor-9---thread name:pool-1-thread-3
time = 1711718343204--task name:executor-11---thread name:pool-1-thread-2


time = 1711718346216--task name:executor-12---thread name:pool-1-thread-1

time = 1711718346216--task name:executor-14---thread name:pool-1-thread-2
time = 1711718346216--task name:executor-13---thread name:pool-1-thread-3

time = 1711718349226--task name:executor-17---thread name:pool-1-thread-3


time = 1711718349226--task name:executor-15---thread name:pool-1-thread-1
time = 1711718349226--task name:executor-16---thread name:pool-1-thread-2



time = 1711718352241--task name:executor-19---thread name:pool-1-thread-1
time = 1711718352241--task name:executor-20---thread name:pool-1-thread-2
time = 1711718352241--task name:executor-18---thread name:pool-1-thread-3


time = 1711718355242--task name:executor-21---thread name:pool-1-thread-1
time = 1711718355242--task name:executor-22---thread name:pool-1-thread-2

time = 1711718355254--task name:executor-23---thread name:pool-1-thread-3


time = 1711718358245--task name:executor-24---thread name:pool-1-thread-1
time = 1711718358245--task name:executor-25---thread name:pool-1-thread-2

time = 1711718358261--task name:executor-26---thread name:pool-1-thread-3


time = 1711718361250--task name:executor-27---thread name:pool-1-thread-1
time = 1711718361250--task name:executor-28---thread name:pool-1-thread-2

time = 1711718361266--task name:executor-29---thread name:pool-1-thread-3

通过打印出来的日志可以看到,所有任务都在名为name:pool-1-thread-1、pool-1-thread-2、pool-1-thread-3的线程中运行,这与我们设置的线程池大小相符。在这个线程池中,前面三个任务优先执行,后面的任务都在等待。

如果把ExecutorService service = Executors.newFixedThreadPool(3);改为ExecutorService service = Executors.newCachedThreadPool();

time = 1711718860800--task name:executor-0---thread name:pool-1-thread-1
time = 1711718860823--task name:executor-23---thread name:pool-1-thread-24
time = 1711718860825--task name:executor-2---thread name:pool-1-thread-3
time = 1711718860826--task name:executor-25---thread name:pool-1-thread-26
time = 1711718860829--task name:executor-8---thread name:pool-1-thread-9
time = 1711718860825--task name:executor-26---thread name:pool-1-thread-27
time = 1711718860825--task name:executor-7---thread name:pool-1-thread-8
time = 1711718860825--task name:executor-20---thread name:pool-1-thread-21
time = 1711718860825--task name:executor-13---thread name:pool-1-thread-14
time = 1711718860823--task name:executor-22---thread name:pool-1-thread-23
time = 1711718860828--task name:executor-1---thread name:pool-1-thread-2
time = 1711718860828--task name:executor-14---thread name:pool-1-thread-15
time = 1711718860828--task name:executor-19---thread name:pool-1-thread-20
time = 1711718860828--task name:executor-29---thread name:pool-1-thread-30
time = 1711718860828--task name:executor-24---thread name:pool-1-thread-25
time = 1711718860828--task name:executor-5---thread name:pool-1-thread-6
time = 1711718860828--task name:executor-6---thread name:pool-1-thread-7
time = 1711718860828--task name:executor-4---thread name:pool-1-thread-5
time = 1711718860827--task name:executor-10---thread name:pool-1-thread-11
time = 1711718860827--task name:executor-9---thread name:pool-1-thread-10
time = 1711718860827--task name:executor-3---thread name:pool-1-thread-4
time = 1711718860827--task name:executor-12---thread name:pool-1-thread-13
time = 1711718860827--task name:executor-15---thread name:pool-1-thread-16
time = 1711718860827--task name:executor-18---thread name:pool-1-thread-19
time = 1711718860827--task name:executor-27---thread name:pool-1-thread-28
time = 1711718860826--task name:executor-17---thread name:pool-1-thread-18
time = 1711718860826--task name:executor-16---thread name:pool-1-thread-17
time = 1711718860826--task name:executor-11---thread name:pool-1-thread-12
time = 1711718860826--task name:executor-21---thread name:pool-1-thread-22
time = 1711718860826--task name:executor-28---thread name:pool-1-thread-29

可以看到,一瞬间任务就都执行完了,可以想象出,newCachedThread()方式创建的线程池,执行任务时会创建足够多的线程。

 下面我们看一下它们是如何被创建出来的

ExecutorService service = Executors.newFixedThreadPool(3);
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

..........构造函数..........
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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}

构造方法参数说明:

corePoolSize:核心线程数,除非设置核心线程超时-allowCoreThreadTimeOut,否则线程会一直存活在线程池中,即使线程池处于空闲状态。

maximumPoolSize:线程池中允许存在的最大线程数。

workQueue:工作队列,当核心线程都处于繁忙状态时,将任务提交到工作队列中。如果工作队列也超过了容量,会去尝试创建一个非核心线程执行任务。

keepAliveTime:非核心线程处理空闲状态最长时间,超过该值线程将会被回收。

threadFactory:线程工厂类,用于创建线程。

RejectedExecutionHandler:工作队列饱和策略,比如丢弃,抛出异常等。

线程池创建完成后,可通过execute方法提交任务,线程池根据当期运行状态和特定参数对任务进行处理。

线程池执行流程

线程池类型核心线程数最大线程数非核心线程空闲时间工作队列
newFixedThreadPoolspecificspecific0LinkedBlockingQueue
newSingleThreadExecutor110LinkedBlockingQueue
newCachedThreadPool0Integer.MAX_VALUE60SSynchronousQueue
newScheduledThreadPoolspecificINteger.MAX_VALUE0DelayedWorkQueue

specific表示使用者传入的固定值。

阻塞队列

为什么要用阻塞队列?

阻塞队列常用于生产者-消费者模型,任务的添加是生产者,任务的调度执行是消费者,他们通常在不同的线程中,如果使用非阻塞队列,那么就需要使用额外的处理同步策略和线程间唤醒策略。比如当前任务队列为空时,消费者线程取元素会被阻塞,当有新的任务添加到队列中时,需要唤醒消费者线程处理任务。

阻塞队列的实现就是在添加元素和获取元素时设置了各种锁操作。

同时,非核心线程是根据阻塞队列的容量进行创建的。具体点就是当阻塞队列未满时,并不会创建非核心线程,而是将任务继续添加到阻塞队列后面等待核心线程执行。

LinkedBlockingQueue:内部使用链表实现的阻塞队列,默认构造函数使用Integer.MAX_VALUE作为容量,另外可通过带capacity参数的构造函数限制容量,使用Executors工具类创建的线程池容量均为Integer.MAX.

SynchronousQueue:容量为0,每当有任务添加进来时会立即触发消费,即每次插入操作一定伴随一个移除操作,反之亦然。

DelayedWorkQueue:用数组实现的,默认容量是16,支持动态扩容,可对延迟任务进行排序,类似优先级队列,搭配ScheduledThreadPoolExecutor可定时或延迟任务。

ArrayBlockingQueue:它不在上述线程池体系中,是基于数组实现,容量固定且不可扩容。

使用场景:

newFixedThreadPool:它的特点是没有非核心线程,这意味着即使任务过多也不会创建新的线程,即使任务闲置也仍然保留一定数量的核心线程,等待队列无线,性能相对稳定,适用于长期有任务要执行,同时任务量也不大的场景。

newSingleThreadExecutor:相当于线程数量为1的newFixedThreadPool,因为线程数量为1,所以适用于任务需要顺序执行的场景。

newCachedThreadPool:它的特点是没有核心线程,非核心线程无线,可短时间内处理无限多的任务,但实际上创建线程十分消耗资源,过多的创建线程可能导致oom,同时该线程池还设置了超时时间,还涉及到线程资源的释放,大量任务并行时性能不稳定,少量任务并行且后续不再执行其他任务的场景可用。

newScheduledThreadPool:通常用于定时或延迟任务。

实际项目开发过程中,不建议直接使用Executors提供的发放创建线程池,如果任务规模,响应时间大致确定,应根据实际需求通过ThreadPoolExecutor各种构造函数手动创建,自由控制线程数、超时时间、阻塞队列、饱和策略等内容(默认饱和策略都是AbortPolicy即抛出异常)。

饱和策略

DiscardPolicy:将丢弃被拒绝的任务

DiscardOldestPolicy:将丢弃队列头部的任务,即先入队的任务会出队以腾出空间

AbortPolicy:抛出RejectedExecutionException异常

CallerRunsPolicy:在execute方法的调用线程中运行被拒绝的任务。

用户也可以通过实现RejectedExecutionHandler接口自定义饱和策略,并通过ThreadPoolExecutor的构造函数传入。

线程池的继承结构

Executor:基类接口,仅定义一个execute方法。

ExecutorService:继承了Executor的接口,定义了带返回值的任务提交方法submit,以及关闭线程池的shutdown方法

AbstractExecutorService:实现了ExecutorService的大部分接口,剩余shutdown与execute方法为实现

ThreadPoolExecutor:常用线程池类

ScheduledThreadPoolExecutor:定义了一系列支持延迟执行任务的线程池

ForkJoinPool:它采用分治思想,将一个任务细分为多个子任务在多线程中执行。

线程池大小选定

需要了解任务是CPU密集型还是IO密集型

CPU密集型:比如大量的计算任务,CPU占用率高,那么此时如果多开线程反而会因为CPU频繁做线程调度导致性能降低。一般建议线程数为CPU核心数+1,+1是为了防止某个核心线程阻塞或意外中断时作为候补。

IO密集型:通常指文件IO、网络IO等。线程数的选取与IO耗时和CPU耗时的比例有关,最佳线程数=CPU核数*[1+(IO耗时/CPU耗时)],之所以设置比例是为了使IO设备和CPU的利用率都达到最高。

线程池的状态

线程池的状态在整个任务处理过程中至关重要,比如添加任务时会先判断线程池是否处于运行状态,任务添加到队列后再判断运行状态,如果此时线程池已经关闭则移除任务并执行饱和策略。

RUNNING:能接收新任务,并且也能处理阻塞队列中的任务

SHUTDOWN:关闭状态,不能接受新任务,但可以处理阻塞队列中已保存的任务。

STOP:不能接受新任务,也不能处理队列中的人物,会中断正在处理任务的线程。

TIDYING:如果所有任务已经终止,workerCount为0,线程池进入该状态后会调用terminated方法进入TERMINATED状态

TERMINATED:在terminated方法执行完之后进入该状态,默认terminated方法中什么也没有做。

参考:Java线程池工作原理浅析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1557088.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Clip算法解读

论文地址&#xff1a;https://arxiv.org/pdf/2103.00020.pdf 代码地址&#xff1a;https://github.com/OpenAI/CLIPz 中文clip代码&#xff1a;https://gitcode.com/OFA-Sys/Chinese-CLIP/overview 一、动机 主要解决的问题&#xff1a; 超大规模的文本集合训练出的 NLP 模…

vue属性与方法

vue属性与方法 计算属性v-model指令——表单的实现样式绑定 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&g…

前端面试拼图-数据结构与算法(二)

摘要&#xff1a;最近&#xff0c;看了下慕课2周刷完n道面试题&#xff0c;记录下... 1. 求一个二叉搜索树的第k小值 二叉树(Binary Tree) 是一棵树 每个节点最多两个子节点 树节点的数据结构{value, left?, right?} 二叉树的遍历 前序遍历&#xff1a;root→left→right 中…

Java类与对象:从概念到实践的全景解析!

​ 个人主页&#xff1a;秋风起&#xff0c;再归来~ 文章专栏&#xff1a;javaSE的修炼之路 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 1、类的定义格式 在java中定义类时需要用到…

记录一个写自定义Flume拦截器遇到的错误

先说结论&#xff1a; 【结论1】配置文件中包名要写正确 vim flume1.conf ... a1.sources.r1.interceptors.i1.type com.atguigu.flume.interceptor.MyInterceptor2$MyBuilder ... 标红的是包名&#xff0c;表黄的是类名&#xff0c;标蓝的是自己加的内部类名。这三个都…

大话设计模式之工厂模式

工厂模式&#xff08;Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一种创建对象的最佳方式&#xff0c;而无需指定将要创建的对象的确切类。通过使用工厂模式&#xff0c;我们可以将对象的创建和使用分离&#xff0c;从而使代码更具灵活性和可维护性。…

Python之Opencv教程(1):读取图片、图片灰度处理

1、Opencv简介 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个用于计算机视觉和图像处理的开源库&#xff0c;提供了丰富的图像处理、计算机视觉和机器学习功能。它支持多种编程语言&#xff0c;包括C、Python、Java等&#xff0c;广泛应用于图像处…

Unity 学习日记 13.地形系统

下载源码 UnityPackage 1.地形对象Terrain 目录 1.地形对象Terrain 2.设置地形纹理 3.拔高地形地貌 4. 绘制树和草 5.为地形加入水 6.加入角色并跑步 7.加入水声 右键创建3D地形&#xff1a; 依次对应下面的按钮 || 2.设置地形纹理 下载资源包 下载资源包后&#x…

【微服务】软件架构的演变之路

目录 单体式架构的时代单体式架构(Monolithic)优点缺点适用场景单体式架构面临诸多问题1.宽带提速&#xff0c;网民增多2.Web2.0时代的特点问题描述优化方向 集群优点缺点适用场景搭建集群后面临诸多问题用户请求问题用户的登录信息数据查询 改进后的架构 垂直架构优点缺点 分布…

OSPF基本原理和概念

文章目录 背景知识OSPF协议概述&#xff1a;OSPF区域的表示OSPF 骨干区域 –区域0OSPF 非骨干区域 -非0区域OSPF的五种区域类型OSPF工作原理OSPF 的报文类型OSPF邻居表中的七个状态 总结 背景知识 一台路由设备如何获取其他网段的路由&#xff0c;并加入到路由表中 直连路由 …

【Java】LinkedList模拟实现

目录 整体框架IMyLinkedList接口IndexNotLegalException异常类MyLinkedList类成员变量(节点信息)addFirst(头插)addLast(尾插)在指定位置插入数据判断是否存在移除第一个相等的节点移除所有相等的节点链表的长度打印链表释放回收链表 整体框架 IMyLinkedList接口 这个接口用来…

IDE/VS2015和VS2017帮助文档MSDN安装和使用

文章目录 概述VS2015MSDN离线安装离线MSDN的下载离线MSDN安装 MSDN使用方法从VS内F1启动直接启动帮助程序跳转到了Qt的帮助网页 VS2017在线安装MSDN有些函数在本地MSDN没有帮助&#xff1f;切换中英文在线帮助文档 概述 本文主要介绍了VS集成开发环境中&#xff0c;帮助文档MS…

常关型p-GaN栅AlGaN/GaN HEMT作为片上电容器的建模与分析

来源&#xff1a;Modeling and Analysis of Normally-OFF p-GaN Gate AlGaN/GaN HEMT as an ON-Chip Capacitor&#xff08;TED 20年&#xff09; 摘要 提出了一种精确基于物理的解析模型&#xff0c;用于描述p-GaN栅AlGaN/GaN高电子迁移率晶体管&#xff08;HEMT&#xff09…

初步了解C++

目录 一&#xff1a;什么是C&#xff1f; 二.C发展史 三:C关键字 四&#xff1a;命名空间 4.1命名空间的介绍 4.2命名空间的使用 4.3命名空间的使用 4.3.1使用作用域限定符 4.3.2 使用using将命名空间的某个成员引入 4.3.3使用using把整个命名空间展开 4.4命名空…

Golang生成UUID

安装依赖 go get -u github.com/google/uuid文档 谷歌UUID文档 示例 函数签名func NewV7() ( UUID ,错误) func (receiver *basicUtils) GenerateUUID() uuid.UUID {return uuid.Must(uuid.NewV7()) } uid : GenerateUUID()

鸿蒙ARKTS--简易的购物网站

目录 一、media 二、string.json文件 三、pages 3.1 登录页面:gouwuPage.ets 3.2 PageResource.ets 3.3 商品页面:shangpinPage.ets 3.4 我的页面:wodePage.ets 3.5 注册页面:zhucePage.ets 3. 购物网站主页面:gwPage.ets 一、media 图片位置:entry > src …

cron服务

Cron文件&#xff1a;Cron服务使用一个特定的配置文件来存储任务和其执行计划。在Unix系统上&#xff0c;这个文件通常是 /etc/crontab&#xff0c; 或者是位于/etc/cron.d/目录下的其他文件。 这些文件包含了任务的定义&#xff0c;包括执行时间和要执行的命令。 类似于 编…

刷爆LeetCode:两数之和 【1/1000 第一题】

&#x1f464;作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任大厂数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python 作者专栏每日更新&#xff1a;LeetCode解锁1000题: 打怪升级之旅https://blog.csdn.net/cciehl/category…

Scala介绍与环境搭建

Scala环境搭建与介绍 一、Scala环境搭建 1、环境准备与下载 2、验证Scala 3、IDEA新建项目&#xff0c;配置Scala&#xff0c;运行Hello world 二、Scala介绍 1、Scala 简介 2、Scala 概述 一、Scala环境搭建 1、环境准备与下载 JDK1.8 Java Downloads | Oracle 下载需求版本…

java项目通用Dockerfile

创建Dockerfile文件&#xff0c;放到项目根目录下和pom.xml同级别 仅需修改为自己项目端口号即可&#xff0c;其他的无需改动 FROM openjdk:11.0.11-jre-slimCOPY target/*.jar .EXPOSE 8080ENTRYPOINT java -jar *.jar构建语句(注意末尾的点 . ) docker build -t container…