深度剖析 Java 线程池的核心原理

 2022-09-19
原文地址:https://blog.51cto.com/u_11864323/5674516

初识线程池

我们考虑一个问题,我如果需要同时做很多事情,是不是给每一个事件都开启一个线程呢?那如果我的事件无限多呢?频繁地创建/销毁线程,CPU该吃不消了吧。所以,这时候线程池的概念就来了。我们举个例子来阐述一下线程池大致工作原理。

比如,有个老板戚总开了个饭店,每到中午就有很多人点外卖,一开始戚总招了10个人送外卖,然而由于午饭高峰期可能同时需要派送50份外卖,那如何保证高效地运行呢?

戚总想着那再招40个员工送?我去,那我这店岂不是要赔死,人员工资这么高,并且大部分时候也只需要同时派送几份外卖而已,招这么多人干瞪眼啊,是啊。但我还得保证高峰期送餐效率,咋办呢?

经过一番思想斗争,戚总想通了,我也不可能做到完美,尽量高效就行了,那正常时间一般只需要同时送四五家外卖,那我就招5个员工作为正式员工(核心线程),再招若干兼职(非核心线程)在用餐高峰时缓解一下送餐压力即可。

那么,人员分配方案出来了,当正式员工(核心线程)空闲时有单进来理所应当让他们派送,如果正式员工忙不过了,就让兼职人员(非核心线程)送,按单提成唄。

好吧,啰嗦这么多,这就是线程池的概念原理吧。

线程池的优点

  • 重用线程池中的线程,避免频繁地创建和销毁线程带来的性能消耗;
  • 有效控制线程的最大并发数量,防止线程过大导致抢占资源造成系统阻塞;
  • 可以对线程进行一定地管理。

线程池的使用

通常开发者都是利用 Executors 提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的 ExecutorService 类型或者不同的初始参数,Executors 目前提供了 5 种不同的线程池创建配置。

ThreadPoolExecutor

ExecutorService 是最初的线程池接口,ThreadPoolExecutor 类是 对线程池的具体实现 ,它通过构造方法来配置线程池的参数,我们来分析一下它常用的构造函数吧。

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

参数解释:

  • corePoolSize,线程池中核心线程的数量, 默认情况下,即使核心线程没有任务在执行它也存在的 ,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutorallowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime 来设定,即keepAliveTime 时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut 默认为false,核心线程没有超时时间。
  • maximumPoolSize,线程池中的最大线程数, 当任务数量超过最大线程数时其它任务可能就会被阻塞最大线程数 = 核心线程 + 非核心线程 。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。
  • keepAliveTime,非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut 设置为true 时,此属性也作用在核心线程上。
  • unit,枚举时间单位,TimeUnit
  • workQueue,线程池中的任务队列,我们提交给线程池的runnable 会被存储在这个对象上。

线程池的分配遵循这样的规则:

  • 当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务;
  • 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行;
  • 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务;
  • 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor 会调用RejectedtionHandlerrejectedExecution 方法来通知调用者。

FixedThreadPool

通过 ExecutorsnewFixedThreadPool() 方法创建,它是个 线程数量固定的线程池,该线程池的线程全部为核心线程 ,它们 没有超时机制且排队任务队列无限制,因为全都是核心线程,所以响应较快,且不用担心线程会被回收

    //newFixedThreadPool 源码,参数 `nThreads`,就是我们固定的核心线程数量
    
    public static ExecutorService newFixedThreadPool(int nThreads){
        return new ThreadPoolExecutor(
            nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>() 
            );
    }
    
    ExecutorService mExecutor = Executors.newFixedThreadPool(5);

CachedThreadPool

通过 ExecutorsnewCachedThreadPool() 方法来创建,它是一个 数量无限多的线程池 ,它 所有的线程都是非核心线程 ,当有新任务来时如果没有空闲的线程则直接创建新的线程不会去排队而直接执行,并且超时时间都是 60s,所以此线程池 适合执行大量耗时小的任务 。由于设置了超时时间为 60s,所以当线程空闲一定时间时就会被系统回收,所以 理论上该线程池不会有占用系统资源的无用线程。

一种用来处理大量短时间工作任务的线程池 ,具有几个鲜明特点:

  • 它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
  • 如果线程闲置的时间超过 60 秒,则被终止并移出缓存;
  • 长时间闲置时,这种线程池,不会消耗什么资源。
  • 其内部使用 SynchronousQueue 作为工作队列
    public static ExecutorService new CachedThreadPool(){
        return new ThreadPoolExecutor(
            0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>()
        );
    }

ScheduledThreadPool

通过 ExecutorsnewScheduledThreadPool() 方法来创建,ScheduledThreadPool 线程池像是上两种的合体,它有 数量固定的核心线程且有数量无限多的非核心线程 ,但是它的非核心线程超时时间是 0s,所以非核心线程一旦空闲立马就会被回收。这类线程池 适合用于执行定时任务和固定周期的重复任务。

    //参数corePoolSize是核心线程数量
    public static ScheduledThreadPool newScheduledThreadPool(int corePoolSize){
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize){
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

SingleThreadExecutor

通过 ExecutorsnewSingleThreadExecutor() 方法来创建,它 内部只有一个核心线程,它确保所有任务进来都要排队按顺序执行 。它的意义在于**,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。**

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

newWorkStealingPool(int parallelism)

这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool,利用Work-Stealing 算法,并行地处理任务,不保证处理顺序。

线程池一般用法

  • shutDown(),关闭线程池,需要执行完已提交的任务;
  • shutDownNow(),关闭线程池,并尝试结束已提交的任务;
  • allowCoreThreadTimeOut(boolen),允许核心线程闲置超时回收;
  • execute(),提交任务无返回值;
  • submit(),提交任务有返回值;

execute()方法

接收一个 Runnable 对象作为参数,异步执行。

    Runnable myRunnable = new Runnable() {
        @Override
        public void run() {
            Log.i("myRunnable", "run");
        }
    };
    mExecutor.execute(myRunnable);