首页 > 编程语言 >Java之线程池

Java之线程池

时间:2024-07-20 19:27:59浏览次数:9  
标签:Java int 创建 队列 任务 线程 执行

一、什么是线程池,为什么使用线程池?

线程池其实是一种池化的技术的实现,实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销

在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务

线程池的优点:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

1、常见线程池

一般都是通过new Thread()来进行线程的创建,但是这样会有一些问题,如:

  1. 每次创建的new Thread()新建的对象性能差

  2. 线程缺乏统一的管理,可能无限制的创建新线程,相互之间竞争,极可能占用系统资源过低导致死机或OOM

  3. 缺乏功能,如定时执行、定期执行和线程中断

相比于new Thread(),Java提供的四种线程池的好处在于

  1. 重用存在的线程,减少对象的创建、消亡的开销、性能佳

  2. 可有效地控制最大并发线程数,提高系统资源的使用率,同时避免过多的资源竞争,避免堵塞

  3. 提供定时执行、定期执行、单线程、并发数控制等功能

Java 定义了 Executor 接口并在该接口中定义了 execute() 用于执行一个线程任务,然后通过 ExecutorService 实现 Executor 接口并执行具体的线程操作。

ExecutorService 接口有多个实现类可用于创建不同的线程池,如下表:

(1)newCachedThreadPool

创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
 for (int i = 0; i < 10; i++) {
     final int index = i;
     try {
         Thread.sleep(index * 1000);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
 ​
     cachedThreadPool.execute(new Runnable() {
 ​
         @Override
         public void run() {
             System.out.println(index);
         }
     });
 }

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程

(2) newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
 for (int i = 0; i < 10; i++) {
     final int index = i;
     fixedThreadPool.execute(new Runnable() {
 ​
 ​
         @Override
         public void run() {
             try {
                 System.out.println(index);
                 Thread.sleep(2000);
             } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
         }
     });
 }

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。

(3) newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {
 ​
     @Override
     public void run() {
         System.out.println("delay 3 seconds");
     }
 }, 3, TimeUnit.SECONDS);

定期执行示例代码如下:

 scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
     @Override
     public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);

表示延迟1秒后每3秒执行一次。 ScheduledExecutorService比Timer更安全,功能更强大,后面会有一篇单独进行对比。

(4) newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
 for (int i = 0; i < 10; i++) {
     final int index = i;
     singleThreadExecutor.execute(new Runnable() {
 ​
         @Override
         public void run() {
             try {
                 System.out.println(index);
                 Thread.sleep(2000);
             } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
         }
     });
 }

2、禁止使用 Executors 创建线程池

可以看到上述四个线程池都是用Executors来创建的,Executors 大大的简化了我们创建各种类型线程池的方式,为什么还不让使用呢?

阿里巴巴Java开发手册说明:

线程池不允许使用Executors,而是通过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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}

ThreadPoolExecutor有7个核心参数,分别了解下:

序号参数名称参数解释
1corePoolSize表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁
2maximumPoolSize表示线程池能够容纳可同时执行的最大线程数
3keepAliveTime表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下 corePoolSize 个线程位置
4unitkeepAliveTime时间单位,最终都会转换成【纳秒】,因为CPU的执行速度杠杠滴
5workQueue当请求的线程数大于 maximumPoolSize 时,线程进入该阻塞队列
6threadFactory顾名思义,线程工厂,用来生产一组相同任务的线程,同时也可以通过它增加前缀名,虚拟机栈分析时更清晰
7handler执行拒绝策略,当 workQueue 达到上限,就要通过这个来处理,比如拒绝,丢弃等,这是一种限流的保护措施

ThreadPoolExecutor 提供了四种拒绝策略:

  1. AbortPolicy默认的拒绝策略,会 throw RejectedExecutionException 拒绝

  2. CallerRunsPolicy:提交任务的线程自己去执行该任务

  3. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列

  4. DiscardPolicy:相当大胆的策略,直接丢弃任务,没有任何异常抛出

不同的框架(Netty,Dubbo)都有不同的拒绝策略,我们也可以通过实现 RejectedExecutionHandler 自定义的拒绝策略

再来看一下newFixedThreadPool的静态方法参数

public static ExecutorService newFixedThreadPool(int nThreads, 
       ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads, 
                                      0L, TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue(), threadFactory);
}

而 LinkedBlockingQueue传入的workQueue 是一个边界为 Integer.MAX_VALUE 队列,也就是无界队列了,那么等待队列也是非常消耗内存的,可能堆积大量的请求,从而导致 OOM

public LinkedBlockingQueue() {
  this(Integer.MAX_VALUE);
}

这里使用的默认拒绝策略

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}

那为什么不能使用默认拒绝策略呢?

因为对于不可预估的高并发量,比较重要的请求时直接拒绝肯定是不合理的,那么选择合理的拒绝策略是必不可少的步骤

对于采用何种策略,具体要看执行的任务重要程度。

如果是一些不重要任务,可以选择直接丢弃;

如果是重要任务,可以采用降级(所谓降级就是在服务无法正常提供功能的情况下,采取的补救措施)

例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿

Executors 返回线程池对象也有OOM的风险:

  • FixedThreadPool 和 SingleThreadExecutor :使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool :使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

  • ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM

从上可以看出:应使用有界队列,控制线程创建数量

因此在项目开发中,使用ThreadPoolExecutor创建线程池,禁止使用 Executors 创建线程池

二、总结

当需要频繁的创建线程时,通过线程池统一管理线程资源,避免不可控风险以及额外的开销

标签:Java,int,创建,队列,任务,线程,执行
From: https://blog.csdn.net/qq_36451127/article/details/140574107

相关文章

  • Java中的Heap(堆)(如果想知道Java中有关堆的知识点,那么只看这一篇就足够了!)
        前言:(Heap)是一种特殊的完全二叉树,它在诸多算法中有着广泛的应用,本文将详细介绍Java中的堆。✨✨✨这里是秋刀鱼不做梦的BLOG✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客先让我们看一下本文大致的讲解内容:目录1.堆的初识       ......
  • 自学Java第三周
    本周学习一、循环1.while循环先判断条件,符合再执行。while(){}2.dowhile循环先执行一次,再判断条件。do{}while();3.forfor(初始条件;判断条件;循环迭代语句){}4.嵌套循环各种类型的循环都可以做内外侧循环。for(){for(){......;}}for(){while(){......;}}...5.控制循环结构break:完全结束一个循环,跳出循环体。continue:结束本次循环,开始下次循......
  • Java--抽象类
    目录抽象类的概念抽象类的语法抽象类的作用抽象类的概念在面向对象的概念中,所有的对象都是通过类来描述的,但是反过来,并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。我们之前所学习的Animal类或者Shape类,就可......
  • java进阶(面向对象实例代码)
    1.抽象类和接口抽象类示例abstractclassAnimal{abstractvoidmakeSound();voidsleep(){System.out.println("Sleeping...");}}classDogextendsAnimal{@OverridevoidmakeSound(){System.out.println("Bark&......
  • 类明显存在却报 package not found, Java程序中专门被其他工程所依赖的common jar用sp
    先上官方链接:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/maven-plugin/examples/repackage-classifier.html在使用SpringBoot构建通用JAR库时,尤其是当通springboot默认的过spring-boot-maven-plugin插件打包时。如果遇到了类存在但报“packagenotfound......
  • Java基础语法01-运算符&流程控制语句If
    Java基础语法1.运算符1.1算术运算符(理解)1.1.1运算符和表达式运算符:对常量或者变量进行操作的符号表达式:用运算符把常量或者变量连接起来符合java语法的式子就可以称为表达式。​不同运算符连接的表达式体现的是不同类型的表达式。举例说明:inta=10;intb=2......
  • Java基础语法02——While循环和Switch
    4.switch语句4.1switch语句结构(掌握)格式switch(表达式){ case1: 语句体1; break; case2: 语句体2; break; ... default: 语句体n+1; break;}执行流程:首先计算出表达式的值其次,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中......
  • JAVA 基础数据类型
    一、数据类型Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(WrapperClass),有些地方也翻译为外......
  • JavaScript - jSignature移动端手写签名
    <html><head><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jSignature/2.1.3/jSignature.min.js"></script>......
  • javascript条件判断语句。
    if语句条件满足就执行,不满足就不执行if(条件){语句}ifelse语句条件满足,执行语句1,条件不满足,执行语句2if(条件){语句1}else{语句2}ifelseifelseif… if(条件1){ 语句1 }else{ 语句2 }if(条件2){ 语句2 }el......