线程池详解:三大方法、七大参数、四大拒绝策略

目录

引言

一、线程池的三大创建方法

1.1 newFixedThreadPool:固定大小线程池

1.2 newSingleThreadExecutor:单线程线程池

1.3 newCachedThreadPool:缓存线程池

二、线程池的七大核心参数

2.1 corePoolSize:核心线程数

2.2 maximumPoolSize:最大线程数

2.3 keepAliveTime:线程空闲时间

2.4 unit:空闲时间单位

2.5 workQueue:任务队列

2.6 threadFactory:线程工厂

2.7 handler:拒绝策略

三、线程池的四大拒绝策略

3.1 AbortPolicy:直接抛出异常

3.2 CallerRunsPolicy:调用者运行

3.3 DiscardPolicy:直接丢弃任务

3.4 DiscardOldestPolicy:丢弃队列中最旧的任务

思考:

四、总结


引言

在多线程编程中,线程池是一种非常重要的技术,它可以有效地管理线程的生命周期,减少线程创建和销毁的开销,提高系统的性能和稳定性。Java通过java.util.concurrent包提供了强大的线程池支持。本文将详细介绍线程池的三大创建方法七大核心参数以及四大拒绝策略,帮助你深入理解并正确使用线程池。

池化技术

前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?

池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化 技术可以大大的提高资源的利用率,提升性能等。

在编程领域,比较典型的池化技术有:

线程池、连接池、内存池、对象池等

主要来介绍一下其中比较简单的线程池的实现原理,希望读者们可以举一反三,通过对线程池的理解, 学习并掌握所有编程中池化技术的底层原理。

我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核 CPU。当一个任务结束,当前线程就接收。

但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成 很大的性能开销。

那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。

这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接 获取,避免多次重复创建、销毁带来的开销。

为什么使用线程池?

10 年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球 ,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。

线程池的优势:

线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这 些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中 取出任务来执行。

它的主要特点为:线程复用,控制最大并发数,管理线程。

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

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

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

一、线程池的三大创建方法

Java中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors, ExecutorService,ThreadPoolExecutor 这几个类。

Java提供了三种常用的线程池创建方法,通过Executors工具类可以快速创建线程池。这些方法适用于不同的场景,但需要注意它们的特性和潜在问题。

1.1 newFixedThreadPool:固定大小线程池

  • 特点:创建一个固定大小的线程池,线程池中的线程数量始终不变。

  • 适用场景:适用于负载比较稳定的服务器,能够控制线程的最大并发数。

  • 潜在问题:队列使用的是无界队列(LinkedBlockingQueue),如果任务提交速度远大于任务处理速度,可能导致内存溢出。 

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 池子大小 5
        ExecutorService threadPool = 
Executors.newFixedThreadPool(5);
        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(()->{
                   
 System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
 }

1.2 newSingleThreadExecutor:单线程线程池

  • 特点:创建一个只有一个线程的线程池,所有任务按顺序执行。

  • 适用场景:适用于需要保证任务顺序执行的场景。

  • 潜在问题:同样使用无界队列,可能导致内存溢出。

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 有且只有一个固定的线程
        ExecutorService threadPool = 
Executors.newSingleThreadExecutor();
        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有1个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(()->{
                   
 System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
 }

1.3 newCachedThreadPool:缓存线程池

  • 特点:创建一个可缓存的线程池,线程池中的线程数量不固定,空闲线程会被回收,新任务会创建新线程。可扩容,遇强则强。

  • 适用场景:适用于执行大量短期异步任务的场景。

  • 潜在问题:线程数量没有上限,可能导致线程数量过多,耗尽系统资源。

 public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 一池N线程,可扩容伸缩
        ExecutorService threadPool = 
Executors.newCachedThreadPool();
        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有N个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                // 模拟延时看效果
                // try {
                //      TimeUnit.SECONDS.sleep(1);
                // } catch (InterruptedException e) {
                //      e.printStackTrace();
                //  }
                threadPool.execute(()->{
                   
 System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
 }

二、线程池的七大核心参数

虽然Executors提供了快速创建线程池的方法,但在实际开发中,更推荐使用ThreadPoolExecutor手动创建线程池,以便更好地控制线程池的行为。

查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )

源码:

// 源码
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;
 }

2.1 corePoolSize:核心线程数

  • 作用:线程池中始终保持存活的线程数量。

  • 特点:即使线程处于空闲状态,也不会被回收,除非设置了allowCoreThreadTimeOut

2.2 maximumPoolSize:最大线程数

  • 作用:线程池中允许的最大线程数量。

  • 特点:当任务队列已满且核心线程都在忙时,线程池会创建新线程,直到线程数达到maximumPoolSize

2.3 keepAliveTime:线程空闲时间

  • 作用:当线程池中的线程数量超过corePoolSize时,空闲线程的存活时间。

  • 特点:超过空闲时间的线程会被回收,直到线程数降到corePoolSize

2.4 unit:空闲时间单位

  • 作用keepAliveTime的时间单位,如TimeUnit.SECONDSTimeUnit.MILLISECONDS等。

2.5 workQueue:任务队列

  • 作用:用于存放待执行任务的阻塞队列。

  • 常用队列

    • LinkedBlockingQueue:无界队列,可能导致内存溢出。

    • ArrayBlockingQueue:有界队列,需要指定队列大小。

    • SynchronousQueue:不存储元素的队列,每个插入操作必须等待一个移除操作。

2.6 threadFactory:线程工厂

  • 作用:用于创建新线程的工厂。

  • 特点:可以自定义线程的名称、优先级等属性。

2.7 handler:拒绝策略

  • 作用:当任务队列已满且线程数达到maximumPoolSize时,如何处理新提交的任务。

  • 四大拒绝策略:见下文。

三、线程池的四大拒绝策略

首先要理解ThreadPoolExecutor 底层工作原理:

当线程池无法处理新任务时(阻塞队列已满且线程数达到maximumPoolSize),会触发拒绝策略。

举例:8个人进银行办理业务

1、1~2人被受理(核心大小core)

2、3~5人进入队列(Queue)

3、6~8人到最大线程池(扩容大小max)

4、再有人进来就要被拒绝策略接受了。

流程:

一. 在创建了线程池后,开始等待请求。

二. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

      1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务:

      2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列:

      3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非 核心线程立刻运行这个任务;

      4. 如果队列满了且正在运行的线程数量大于或等于1Size,那么线程池会启动饱和拒绝策略来执 行。

三. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

四. 当一个线程无事可做超过一定的时间(keepA1iveTime)时,线程会判断:

      如果当前运行的线程数大于coreP佣1Size,那么这个线程就被停掉。

      所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

Java提供了四种内置的拒绝策略:

3.1 AbortPolicy:直接抛出异常

  • 特点:直接抛出RejectedExecutionException,阻止系统正常运行。

  • 适用场景:需要快速失败并明确知道任务被拒绝的场景。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

3.2 CallerRunsPolicy:调用者运行

  • 特点:将任务回退给调用者线程执行。

  • 适用场景:适合需要保证任务一定被执行的场景。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

3.3 DiscardPolicy:直接丢弃任务

  • 特点:直接丢弃新提交的任务,不做任何处理。

  • 适用场景:适合对任务丢失不敏感的场景。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardPolicy()
);

3.4 DiscardOldestPolicy:丢弃队列中最旧的任务

  • 特点:丢弃队列中最旧的任务,然后尝试重新提交新任务。

  • 适用场景:适合允许丢弃旧任务的场景。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardOldestPolicy()
);

思考:

线程池用哪个?生产中如何设置合理参数?

在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多? 坑

答案是一个都不用,我们工作中只能使用自定义的;Executors 中 JDK 已经给你提供了,为什么不用?

线程是否越多越好?

一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu 核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。

但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间 的。

因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。

如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在 CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的 话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。

所以开多线程,比 方说多线程网络传输,多线程往不同的目录写文件,等等。

此时 线程数等于IO任务数是最佳的。

四、总结

线程池是多线程编程中的核心工具,合理地使用线程池可以显著提高系统的性能和稳定性。通过本文的介绍,你应该已经掌握了线程池的三大创建方法、七大核心参数以及四大拒绝策略。在实际开发中,要具体需求手动配置线程池,避免使用Executors的默认方法,从而更好地控制线程池的行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值