java线程池详解

news2025/8/9 21:02:25

一 介绍

线程我们可以使用 new 的方式去创建,但如果并发的线程很多,每个线程执行的时间又不长,这样频繁的创建线程会大大的降低系统处理的效率,因为创建和销毁进程都需要消耗资源,线程池就是用来解决类似问题。

线程池实现了一个线程在执行完一段任务后,不销毁,继续执行下一段任务。用《Java并发编程艺术》提到线程池的优点:

1、降低资源的消耗:使得线程可以重复使用,不需要在创建线程和销毁线程上浪费资源

2、提高响应速度:任务到达时,线程可以不需要创建即可以执行

3、线程的可管理性:线程是稀缺资源,如果无限制的创建会严重影响系统效率,线程池可以对线程进行管理、监控、调优。

二 Excutor框架

Excutor框架是线程池处理线程的核心,包括创建任务,传递任务,任务的执行三个方面

2.1 创建任务

执行的任务需要实现 Runnable 或者 Callable 接口,然后重写里面的 run 方法,这两个接口区别下面会写

2.2 传递任务

以前执行线程都是直接创建线程然后调用 start() 方法去执行线程,现在我们需要把任务传递到线程池里面去,传递任务的核心接口就是 Excutor 接口,而它下面有几个实现的方法,如图所示:
 在这里插入图片描述
 可以看到真正实现了功能的其实就是两个类 ThreadPoolExecutor 和 ScheduledTreadPollExecutor(可以延时定时执行任务),而其中用的最多的就是这个 ThreadPoolExecutor ,下面会详细介绍,我们把任务通过这个方法的对象进行传递。

2.3 任务的执行及返回的结果

任务传递给线程池执行完毕后,对于不同的传递方式,会有不同的返回策略,对于利用 excutor 方法传递的任务,不管执行的怎么样都不会有值传递回来,而对于 submit 方法传递的任务会返回一个 FutureTask 对象,返回用户希望接受的值。
Excutor 框架使用示意图如图:
在这里插入图片描述

三 ThreadPoolExecutor 类介绍

ThreadPoolExecutor 是线程池最为核心的一个类,而线程池为它提供了四个构造方法,我们先来看一下其中最原始的一个构造方法,其余三个都是由它衍生而来

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    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;
    }
 

可以看到这里有6个参数,这些参数直接影响到线程池的效果,以下是具体分析每个参数的意义

  1. corePoolSize:线程池最小创建线程的数目,默认情况下,线程池中是没有线程的,也就是当没有任务来临的时候,初始化的线程池容量为0,而最小创建线程的数目则是在有线程来临的时候,直接创建
    corePoolSize 个线程
  2. maximumPoolSize:线程池能创建的最大线程的数量,在核心线程都被占用的时候,继续申请的任务会被搁置在等待队列里面,而当等待队列满了的时候,线程池就会把线程数量创建至
    maximumPoolSize 个。
  3. workQueue:核心线程被占有时,任务被搁置在任务队列
  4. keepAliveTime:当线程池中的线程数量大于 corePoolSize 时这个参数就会生效,即当大于 corePoolSize
    的线程在经过 keepAliveTime 仍然没有任务执行,则销毁线程
  5. unit :参数keepAliveTime的时间单位
  6. ThreadFactory:线程工厂:主要用来创建线程,一般默认即可
  7. handler:饱和策略,即当线程池和等待队列都达到最大负荷量时,下一个任务来临时采取的策略

在这里插入图片描述
饱和策略的介绍: 即如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

四 实现一个简单的线程池

4.1 创建任务

class worker implements Runnable{
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName());
    }
    
}

4.2 创建线程池,并将任务传递进去

public class Test {
    private static final int corePoolSize = 4;
    private static final int maximumPoolSize = 6;
    private static final long keepAliveTime = 2;
    private static final TimeUnit unit = TimeUnit.SECONDS;
    private static final int QueueSize = 5;//将参数设定为固定值
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                new ArrayBlockingQueue<>(QueueSize),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );//构造线程池
        for(int i = 0; i < 10; i++){
            worker w = new worker();
            executor.execute(w);
        }//传入10个任务
        executor.shutdown();//关闭线程池
        while(!executor.isTerminated()){
    }
        System.out.println("finish");
}}

4.3 运行结果

pool-1-thread-4
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-5
pool-1-thread-1
pool-1-thread-4
pool-1-thread-5
pool-1-thread-3
pool-1-thread-2
finish

我们可以根据运行的结果就知道我传进去的 10 个任务是通过 5 个线程复用所得到。

五 线程池相关问题

5.1 线程池状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

1 volatile int runState;
2 static final int RUNNING    = 0;
3 static final int SHUTDOWN   = 1;
4 static final int STOP       = 2;
5 static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

  • 当创建线程池后,初始时,线程池处于RUNNING状态;
  • 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
  • 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
  • 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

5.2 线程池处理任务的策略

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于
    corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

在这里插入图片描述

5.3 Runnable 和 Callable的区别

Runnable:

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();//单纯的run
}

```go
**Callable**
@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;//无法计算则抛出异常
}

5.4 Excutor 和 Submit的区别

  • Excutor用于提交不需要返回值的任务,线程在执行完后既不会返回结果,也不会抛出异常
  • Submit则是用于提交需要返回值的任务,在线程执行完Submit提交的任务后,会返回一个 future 对象,这个 future
    对象可以用来判断任务是否执行成功,并且可以用 future 的get()方法来获取其返回值,如:
    ExecutorService executorService = Executors.newFixedThreadPool(3);
Future<String> submit = executorService.submit(() -> {
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "abc";//带有返回值
});

String s = submit.get();//get返回值
System.out.println(s);
executorService.shutdown();

六 几种线程池比较

6.1 FixThreadPool

称为可重用固定线程池,其源码:

/**
     * 创建一个可重用固定数量线程的线程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

从源码可以看出,这个这个线程池的核心线程数 和 最大线程数量都设置为 nThreads ,这个参数是我们在创建线程池的时候传递的

在这里插入图片描述
上图说明:

  • 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  • 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
  • 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

使用FixThreadPool的缺点

在 FixThreadPool 中的等待队列是使用的 LinkedBlockingQueue,这是一个无界队列,最大的容量为 Integer.MAX_VALUE,可以无限制的接受任务,从而导致线程池的最大线程数参数生无效的,在核心线程全部被占有时,新来的任务就会被安置在无界队列中,当任务很多的时候,线程池没有拒绝任务的策略,就可能导致OOM(内存不足)

当线程池中的线程数达到核心线程数后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
由于使用无界队列时 最大线程数 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 的源码可以看出创建的 FixThreadPool 的 核心线程数和 最大线程数 被设置为同一个值。
运行中的 FixThreadPool(未执行 shutdown()或 shutdownow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

6.2 SingleThreadExcutor

/**
     *返回只有一个线程的线程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

可以看到特点是只有一个核心线程

在这里插入图片描述
SingleThreadExcutor的缺点

同样的其等待队列也是用的 LinkedBlockingQueue 无界队列,同样也可能出现 OOM。

6.3 CachedThreadPool

/**
     * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

从源码可以看到,其核心线程数为0,但最大线程数目为 Integer.MAX_VALUE,即是无界,其只要来任务需要线程就会创建线程,极端情况会造成线程耗尽的情况
其执行过程是当有任务传递给线程池会先查看现有的线程是否够,如果不够,则会创建新的线程。 其缺点就是可能创建大量线程,导致OOM

七 线程池大小的设置

线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。
很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

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

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

相关文章

餐饮+KTV融合消费模式,会受消费者喜欢吗?

这个五一&#xff0c;我们雨科网门店系统的客户&#xff0c;大侠火锅店终于是将KTV搬到了自己的门店里&#xff0c;运用门店小程序功能及纸质代金券及礼品的噱头吸引客户进店&#xff0c;只需消费并和任意一人合唱一首歌即可领取&#xff0c;消费者在等餐或放松的时候一键点歌演…

一起来了解下 TOWER “未知装置”

从 2022 年 11 月 9 日开始&#xff0c;TOWER 未知装置可在 TOWER Token 网站&#xff08;https://www.crazydefenseheroes.com/fusion/game-assets&#xff09;上使用&#xff0c;允许玩家使用 TOWER Token 融合他们的 NFT 或将他们现有的 TOWER NFT 跟踪为不同的形式。 第一阶…

js - js中关于getBoundingClientRect()方法的使用

介绍一下getBoundingClientRect()方法的一些属性&#xff0c;以及具体的使用场景和一些需要注意的问题&#xff1b; 1&#xff0c;介绍 此方法返回元素的大小及其相对于视口的位置&#xff1b;返回的是一个对象&#xff1a; 包括 left、top、right、bottom、x、y、width 和 he…

ThinkPHP5目录结构

文章目录一、TP5的框架的下载1、[采用fastAdmin安装](https://www.fastadmin.net/download.html)2、Composer安装2.1 Composer提供的服务3、Git安装二、使用Composer安装后目录结构2.1 补充获取 Git 仓库git的工作机制一、TP5的框架的下载 1、采用fastAdmin安装 FastAdmin是一…

数据可视化大屏设计

在数据业务展示场景中&#xff0c;数据可视化大屏已经变得十分常见。那么在设计思路上&#xff0c;数据可视化大屏应当遵循什么样的设计逻辑&#xff1f;本篇文章里做了介绍&#xff0c;一起来看一下。 一、数据大屏的应用场景 1、大型会议 2、业务展示 二、数据大屏分类 1、展…

阿里云新用户滑块验证不过,阿里云滑动验证流程及线上问题排查

滑动验证是阿里巴巴集团提供的一种验证码解决方案&#xff0c;采用风险分析引擎&#xff0c;有效区分来自人类与机器人的访问行为并拦截机器风险&#xff0c;为您提供安全可靠的业务环境。 下图是滑动验证在用户客户端的展示效果。 产品介绍&#xff1a;阿里云验证码&#x…

5.XMLHttpRequest对象

XMLHttpRequest简称xhr&#xff0c;是浏览器提供的Javascript对象。之前我们使用的都是jQuery中的Ajax&#xff0c;现在我们使用原生JS的Ajax 目录 1 GET请求 1.1 不带参数请求 1.2 带参数请求 2 URL的编码与解码 2.1 编码 encodeURI() 2.2 解码 decodeURI() 3 …

计算机网络——TCP连接管理

本文将会介绍如何建立和拆除一条TCP连接&#xff0c;TCP的连接会显著的增加人们感受到的时延&#xff08;尤其是在Web冲浪时&#xff09;。同时一些常见的网络攻击SYN洪范攻击利用了TCP连接管理的弱点。 假定运行一台主机&#xff08;客户&#xff09;上的一个进程想和另一台主…

Spring中各种注解的使用说明汇总清单

目录 一、放在最开始方法的外侧的注解 1、ControllerAdvice&#xff1a; 2、ResponseBody 3、slf4j 4、Configuration 5、Component 6、Mapper 7、Service 8、RestController RequestMapping("/路径") 9、EnableTransactionManagement和Transactional&…

maven学习:maven安装、maven仓库、Idea配置maven

一、maven安装 1.1 下载maven Maven的版本对应的jdk和发布日期:Maven – Maven Releases History 到官方网站下载maven,网站: Maven – Download Apache Maven 如下图: 下载maven之前的版本:Index of /dist/maven/maven-3 注意&#xff1a; idea与maven的版本需要兼容, 所以我…

Python学的好,工作不愁找

俗话说的好&#xff1a;“Python学的好&#xff0c;工作不愁找”&#xff0c;不管我们学习哪一门语言&#xff0c;我们都要掌握从抽象化中提取出来的方法&#xff0c;这样你才能提高我们的学习能力&#xff0c;并且在学习新事物的时候可以提取我们自己的想法。学习Python&#…

基于SpringBoot的仿小米商城系统

仿小米商城分为前后台&#xff0c;分为用户和管理员两种角色&#xff0c;管理员可以登录后台管理系统~ 可以作为毕设~也可以作为学习的项目~ 前台部分运行截图如下所示 后台部分运行截图如下所示 拒绝白嫖&#xff01;20分享源码&#xff01;want to get it 私 me&#xf…

Java入门简单小游戏有哪些?

适合新手练习的小游戏有超级玛丽、愤怒的小鸟、飞机大战、五子棋、彩虹雨、聊天室、华容道、坦克大战、扫雷等。还有贪吃蛇&#xff0c;由Java开发出来的一款经典小游戏&#xff0c;java小白入门可以用这款游戏开发练手锻炼自己的编程能力。 教程推荐&#xff1a;玩会Java&…

【Linux】(六)MobaXterm文件传输与终端-实验室服务器使用MobaXterm传输文件和ssh终端连接

MobaXterm系列文章文件传输或远程终端&#xff08;MobaXterm&#xff09;系列文章 &#xff08;一&#xff09;服务器初次配置及安装vncserver &#xff08;二&#xff09;实验室添加新用户及其配置vnc4server、xfce4桌面访问流程 &#xff08;三&#xff09;VNC桌面连接开发方…

C++初阶(stack+queue)

stack是一种容器适配器&#xff0c;专门用在具有后进先出 (last-in first-out)操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。stack是作为容器适配器被实现的&#xff0c;容器适配器即是对特定类封装作为其底层的容器&#xff0c;并提供一组…

Unity RectTransform 组件

文章目录1.RectTransform 介绍2.Anchor 锚点3.Pivot 轴心点4.RectTransform API 属性4.1 anchorMax、anchorMin —— 锚点矩形4.2 offsetMax、offsetMin —— 偏移量4.3 rect —— 矩形类4.4 sizeDelta —— UI坐标4.5 anchoredPosition —— UI坐标5.RectTransform API 方法5.…

数据可视化 | Tableau从入门到高手 入门联接关系数据预处理

Tableau 人人可用、处处可用的分析 Tableau 是一个可视化分析平台&#xff0c;它改变了我们使用数据解决问题的方式&#xff0c;使个人和组织能够充分利用自己的数据。利用 Tableau 查看并理解任何数据。 Tableau Software 可以帮助人们查看并理解数据&#xff0c;可连接到几乎…

蓝桥杯实战应用【算法代码篇】-基于数组实现线性表

前言 线性表是由有限个具有相同性质的元素构成,结构上要求,非表头和表尾元素有且只有一个前驱和后继。对于表头和表尾元素,如果是没有环的线性表,则表头元素没有前驱,表尾元素没有后继。如果有环则均存在前驱和后继元素。对于线性结构,有两种保存的方法,一种是使用C语言…

Spring Boot Admin -Actuator 图形化管理工具

Spring Boot Admin 概述 之前学习了Spring Boot Actuator的特性&#xff0c;它能够是开发者很便捷的监控系统的各种指标&#xff0c;但是有一个很大的问题 — 不够直观。今天来学习下Spring Boot Admin。Spring Boot Admin 是一个免费的开源社区项目&#xff0c;用于监控、管…

Java多线程:从基本概念到避坑指南

本文为掘金社区首发签约文章&#xff0c;未获授权禁止转载 多核的机器&#xff0c;现在已经非常常见了。即使是一块手机&#xff0c;也都配备了强劲的多核处理器。通过多进程和多线程的手段&#xff0c;就可以让多个CPU同时工作&#xff0c;来加快任务的执行。 多线程&#xf…