Advertisement

所有Java程序员有要懂的线程池:线程池是什么?为什么要用线程池?怎么用线程池?

阅读量:

多线程确实存在诸多挑战。具体而言,在实际应用中涉及线程的创建、终止以及调度等操作都相当复杂。然而,在我们的日常工作中似乎并没有频繁手动new一个新线程的现象出现。实际上这是因为很多现有的编程框架都提供了一个高度封装好的解决方案——即基于多核处理器或高性能服务器时使用的多任务处理机制。

在软件开发中引入多任务处理机制通常依赖于一种称为"进程池"的技术手段。这种技术手段能够协调利用多个独立运行的任务进程,并通过资源分配优化系统性能的同时减少资源浪费。相比于传统的单任务处理模式,在使用进程池的情况下开发人员能够更加专注于核心业务逻辑的设计与实现而不必过问底层细节以确保系统的高效运行。

此外通过采用进程池技术我们可以将注意力集中在如何设计与实现具体的业务逻辑上而不是过分关注细节层面的操作流程从而实现了业务流程与系统底层的具体执行步骤之间的解耦关系。本文将从‘是什么’、‘为什么’以及‘如何实现’三个角度来讲解进程池的工作原理及其实际应用价值。”

线程池是什么
为什么要用线程池
怎么用线程池

同时这里归纳了Java的核心知识点以及30多家公司的真题集。欢迎点击此处参与活动,并在评论区注明暗号

在这里插入图片描述

线程池 Thread Pool

线程池是一种池化的技术,类似的还有数据库连接池、HTTP 连接池等等。
池化的思想主要是为了减少每次获取和结束资源的消耗,提高对资源的利用率。
比如在一些偏远地区打水不方便的,大家会每段时间把水打过来存在池子里,这样平时用的时候就直接来取就好了。
线程池同理,正是因为每次创建、销毁线程需要占用太多系统资源,所以我们建这么一个池子来统一管理线程。用的时候从池子里拿,不用了就放回来,也不用你销毁,是不是方便了很多?
Java 中的线程池是由 juc 即 java.util.concurrent 包来实现的,最主要的就是 ThreadPoolExecutor 这个类。具体怎么用我们下文再说。
线程池的好处
在多线程的第一篇文章中我们说过,进程会申请资源,拿来给线程用,所以线程是很占用系统资源的,那么我们用线程池来统一管理线程就能够很好的解决这种资源管理问题。
比如因为不需要创建、销毁线程,每次需要用的时候我就去拿,用完了之后再放回去,所以节省了很多资源开销,可以提高系统的运行速度。
而统一的管理和调度,可以合理分配内部资源,根据系统的当前情况调整线程的数量。
那总结来说有以下 3 个好处:

降低资源消耗:通过循环使用现有线程来完成任务操作,在不需要频繁启动和终止的情况下减少系统负载。
加快处理速度的原因在于无需启动新线程的过程,在接收任务指令后立即投入运行。
该系统架构提供了额外的功能扩展可能性,在现有配置基础上支持定时和延时地启动相关子进程以实现特定功能需求

说了这么多,终于到了今天的重点,我们来看下究竟怎么用线程池吧~

线程池的实现

Java 给我们提供了 Executor 接口来使用线程池。

在这里插入图片描述

我们常用的线程池有这两大类:

复制代码
    ThreadPoolExecutor
    ScheduledThreadPoolExecutor

它俩的主要区别在于第一个仅是普通的非定时执行方式而第二个具备定时执行功能。
除此之外还有其他类型的线程池例如JDK 1.7版本中首次引入了ForkJoinPool这种设计理念能够将大任务分解为小模块并依次处理最终实现统一整合。
当任务被分配至线程池时它会经历以下具体流程:
首先在线程池内部采用生产者消费者模式将独立分离的任务与运行所需的资源进行解耦从而实现了对资源和服务的有效管理。
随后系统会对这些待处理的任务按照优先级进行排序并依次分配给不同的空闲线程以提升处理效率。
最后所有完成的任务会通过特定机制整合返回给主调用方完成整个流程。

在这里插入图片描述

该系统首先评估核心线程池是否已达到容量上限。核心线程池即一个始终保持在池中的活跃线程数量,在不影响系统性能的前提下动态调节资源分配效率。具体而言,在生产者消费者机制中已有提及。
例如:假设系统的最大吞吐能力是每秒处理100个请求,则我们设定的核心进程数量为80个。这样无论当前负载情况如何变化,在最繁忙时段也能保证至少有80个核心进程保持在线状态以应对负载压力。
此外还需要判断当前的进程数目是否已经达到了最大限制值即100个进程而非80个进程。如果超出该阈值则无法继续创建新进程因此必须采用饱和策略或者拒绝策略来处理超出资源的情况。
饱和策略将在后续章节中详细介绍。

在这里插入图片描述

ThreadPoolExecutor

我们主要说下 ThreadPoolExecutor ,它是最常用的线程池。

在这里插入图片描述

从下面可以看到,在该类共有四个构造函数中,默认情况下会调用最后一个函数。单击查看详细信息,默认情况下会调用最后一个函数。由此可见,在这四个构造函数中,默认情况下会调用最后一个函数。

复制代码
    public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    ...
    }

这里我们来仔细看下这几个参数:

corePoolSize: corePoolSize即为上文曾提及的核心线程池规模参数,在系统运行期间位于核心层管理的多线程环境里运行的一组独立进程。

复制代码
    corePoolSize the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set

maximumPoolSize:线程池的最大容量。

复制代码
    maximumPoolSize the maximum number of threads to allow in the pool

keepAliveTime:存活时间表示此数值。此数值指的是,在线程池运行过程中,在使用了超过核心处理单元的数量后,在多长时间才会被销毁

复制代码
    keepAliveTime when the number of threads is greater than the core,this is the maximum time that excess idle threads will wait for new tasks before terminating.

unit:对应上面存活时间的时间单位。

复制代码
    unit the time unit for the {@code keepAliveTime} argument

workQueue:它是一个阻塞队列,在这种情况下线程池同样遵循着生产者消费者模式这一原则运行。其中的任务相当于生产者角色而线程则扮演消费者的角色因此该阻塞队列的作用在于协调生产与消费的速度

复制代码
    workQueue the queue to use for holding tasks before they are executed.

threadFactory:这里用到了工程模式,用来创建线程的。

复制代码
    threadFactory the factory to use when the executor creates a new
    thread

handler:这个就是拒绝策略。

复制代码
    handler the handler to use when execution is blocked because the thread bounds and queue capacities are reached

所以我们可以利用提供的7个参数自定义一个线程池,并且值得一提的是Java为此已经为我们提供了几类常用线程池供直接使用

复制代码
    newCachedThreadPool
    newFixedThreadPool
    newSingleThreadExecutor

我们具体来看每个的含义和用法。

复制代码
    newCachedThreadPool
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
    }

这里我们可以看到,

核心线程池配置设为零;
最大容量设为Integer.MAX_VALUE;
每个线程的存活时间设定为60秒;也就是如果在1分钟内未被使用,则该线程将被回收;
最后采用了同步队列结构。

它的适用场景在源码里有说:

复制代码
    These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks.

来看怎么用:

复制代码
    public class newCacheThreadPool {
    
    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 向线程池提交任务
        for (int i = 0; i < 50; i++) {
            executorService.execute(new Task());//线程池执行任务
        }
        executorService.shutdown();
    }
    }
在这里插入图片描述

执行结果:

可以很清楚的看到,线程 1、2、3、5、6 都很快重用了。

复制代码
    newFixedThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
    }

这个线程池的特点是:

固定数目是线程池中的线程数量,
它也是我们在创建该线程池时必须提供的输入参数。
当线程数目超过固定数目时,
会在队列中等待处理。

它的适用场景是:

复制代码
    Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.
    
    public class FixedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 200; i++) {
            executorService.execute(new Task());
        }
        executorService.shutdown();
    }
    }
在这里插入图片描述

这里我设置了线程池的最大容量为10个线程,在面对成百上千的任务请求时也只能保证使用其中的前十个位置进行处理。

复制代码
    newSingleThreadExecutor
    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
    }

这个线程池顾名思义,里面只有 1 个线程。
适用场景是:

复制代码
    Creates an Executor that uses a single worker thread operating off an unbounded queue.

我们来看下效果。

复制代码
    public class SingleThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task());
        }
        executorService.shutdown();
    }
    }
在这里插入图片描述

当执行结果生成时,我直观感受到系统存在轻微卡顿现象,在之前的测试案例中并未观察到此问题.在线程池的应用中,默认使用的工具通常是 ThreadPoolExecutor 类别,并通过配置不同的参数来实现功能需求.需要注意的是这两个参数:

一是工作队列的工作模式选择问题。这一项的具体内容主要是阻塞队列的选择问题。如果要进一步深入讨论这个问题的话,请稍后继续交流。
二是handler的配置方案的制定和优化。

注意到,在这三个具体线程池中,并没有设置handler参数的原因是统一采用了资源锁定机制

复制代码
    defaultHandler。
    /** * The default rejected execution handler
     */
    private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();

在 ThreadPoolExecutor 中遵循四种不同的拒绝策略,并且这些策略都实现了## RejectedExecutionHandler接口。

AbortPolicy 表示为一种执行任务并触发RejectedExecutionException的行为。我们将其命名为‘正式拒绝’(Formal Rejection)。DiscardPolicy 则是一种执行任务但无声音输出的行为称为‘默拒’(Silent Rejection)。顾名思义的策略是将旧的任务丢弃以腾出资源给新作业。CallerRunsPolicy 则是由该线程直接执行该任务的策略被称为VIP(Very Important Process)策略。

总结

这三种线程池都采用默认策略即第一种策略,并明确拒绝了请求。另外本文所讲述的知识点相对有限主要涉及线程池的基本使用方法包括...核心概念以及基本操作等基础内容。

在这里插入图片描述

全部评论 (0)

还没有任何评论哟~