一文详解ThreadPoolExecutor与线程池原理(下)

目录

一. ForkJoinPool

1. ForkJoinPool构造方法

2. submit方法

任务是如何提交到工作队列的 

线程是怎么工作的

3. 任务并行处理

​示例

4. CompletableFuture

二. 如何正确设置线程池参数 

1. 动态修改线程池的实现 

2. 线程池监控

三. 总结 

前文介绍了ThreadPoolExecutor和ScheduledThreadPoolExecutor的执行原理和源码分析。本文将继续对JUC包下的ForkJoinPool线程池做深入分析,另外还会介绍如何正确设置线程池参数和对线程池的监控的方法。友情提示ForkJoinPool源码较为枯燥,不感兴趣可直接跳到线程池参数部分。

一. ForkJoinPool

JUC还提供了一个特殊的线程池ForkJoinPool,适用于处理计算密集型任务能充分发挥CPU多核的作用。它的工作特点是将任务分解成更小的子任务,使用分而治之的策略进行操作,使其能够并发的执行任务。再结合工作窃取模式(worksteal)提高整体的执行效率,充分利用CPU资源。

分而治之,是一种解决问题的策略,即将一个复杂的问题分解成若干个规模更小、结构相似的子问题,递归地解决这些子问题,然后将子问题的解合并以得到原问题的解。

比较常见的归并排序,快速排序这些都是常见的分治思想。大数据领域的MapReduce也是一种分治的思想,用于大规模数据集(大于1TB)的并行运算。通过Map和Reduce两个阶段把对大规模数据集的操作,分发给一个主节点管理下的各个分节点共同完成,然后通过整合各个节点的中间结果来得到最终结果。

ForkJoinPool的源码相对于其他的线程池来讲比较复杂,处理的逻辑也比较多。其中为了保证任务并行和任务窃取时的线程安全问题,使用了大量的CAS方法。在阅读源码时十分不友好,所以在这片文章尽可能的就主要逻辑进行讲解。

1. ForkJoinPool构造方法

对于一项新技术的探究老规矩还是从构造方法开始。ForkJoinPool提供了几个不同参数的构造方法也提供了获取static代码块内生成的commonPool。不管使用了哪一个最终都会调用下面的私有构造方法。

    /**
     * Creates a {@code ForkJoinPool} with the given parameters, without
     * any security checks or parameter validation.  Invoked directly by
     * makeCommonPool.
     */
    private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix) {
        //设置线程名称
        this.workerNamePrefix = workerNamePrefix;
        //线程工厂,创建自定义的ForkJoinWorkerThread线程
        this.factory = factory;
        //拒绝策略
        this.ueh = handler;
        this.config = (parallelism & SMASK) | mode;
        //核心线程数,负值是为了配合位运算和后面的状态判定
        long np = (long)(-parallelism); // offset ctl counts
        //又是ctl 保存线程池状态和线程数量的
        this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    }

在构造方法内重点关注线程工厂,它会生成自定义线程ForkJoinWorkerThread,后面的实现原理和它有很大关系。

在介绍前面的两个线程池时它们的构造参数都会有一个指定队列,ForkJoinPool在存储任务时也需要使用队列只不过没有在构造方法中定义。这个队列就是ForkJoinPool的内部类WorkQueue,ForkJoinPool定义任务队列时使用的是WorkQueue数组。也就是说任务是提交给WorkQueue数组的。

volatile WorkQueue[] workQueues;     // main registry

 并且每一个WorkerQueue内部声明了两个重要的变量ForkJoinWorkerThreadForkJoinTask数组

        ForkJoinTask<?>[] array;   // the elements (initially unallocated)
        final ForkJoinWorkerThread owner; // owning thread or null if shared

 通过下面的图可以直观的了解ForkJoinPool使用的队列的结构

2. submit方法

ForkJoinPool这个类也是继承了AbstractExecutorService类,不管它是一个怎么特殊的线程池,也一定支持简单任务的submit处理。这部分针对ForkJoinPool的submit方法做深入探究。

   /**
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public ForkJoinTask<?> submit(Runnable task) {
        if (task == null)
            throw new NullPointerException();
        ForkJoinTask<?> job;
        if (task instanceof ForkJoinTask<?>) // avoid re-wrap
            job = (ForkJoinTask<?>) task;
        else
            //把Runnable转换成封装的对象方便后续执行特殊逻辑
            job = new ForkJoinTask.AdaptedRunnableAction(task);
        //提交任务
        externalPush(job);
        return job;
    }

这部分的逻辑首先把传进来的Runnable任务做封装,装转换成ForkJoinTask.AdaptedRunnableAction。回想一下ScheduledThreadPoolExecutor线程池也是对Runnable任务做封装转换成ScheduledFutureTask来执行延迟逻辑。所以在这里可以猜测ForkJoinPool的并行处理逻辑主要就是在ForkJoinTask之中。

看到这我们可以大致分析一下。线程池工作其实就做了两件事,

  • 一是怎么把任务提交到任务队列里面
  • 二是线程工作时怎么从队列里面把任务取出来执行的

任务是如何提交到工作队列的 

在 externalPush方法中主要根据判断逻辑确定是否完成了队列的初始逻辑,是否有可用队列能够存储ForkJoinTask任务。如果不满足条件的情况需要externalSubmit来处理初始逻辑。

    final void externalPush(ForkJoinTask<?> task) {
        WorkQueue[] ws; WorkQueue q; int m;
        //获取一个随机数
        int r = ThreadLocalRandom.getProbe();
        //获取线程状态
        int rs = runState;
        //workQueues不为空说明已经完成了初始化,且根据随机数定位的index存在workQueue,且cas的方式加锁成功
        //对于第一次调用一定是不满足条件的
        if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
            (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
            U.compareAndSwapInt(q, QLOCK, 0, 1)) {
            ForkJoinTask<?>[] a; int am, n, s;
            if ((a = q.array) != null &&
                (am = a.length - 1) > (n = (s = q.top) - q.base)) {
                int j = ((am & s) << ASHIFT) + ABASE;
                //把任务加入到队列中
                U.putOrderedObject(a, j, task);
                U.putOrderedInt(q, QTOP, s + 1);
                U.putIntVolatile(q, QLOCK, 0);
                if (n <= 1)
                    //通知线程执行任务
                    signalWork(ws, q);
                return;
            }
            U.compareAndSwapInt(q, QLOCK, 1, 0);
        }
        //初始化workQueues
        externalSubmit(task);
    }

在看 externalSubmit之前回忆下之前介绍WorkerQueue数据的结构。要实现添加ForkJoinTask需要经过几个逻辑的判断

  • ForkJoinPool下WorkerQueue数组是否为空
  • WorkerQueue数组的索引位置是否有WorkerQueue对象
  • WorkerQueue下的ForkJoinTask数组是否空并且有空余位置

理解这几个逻辑再观看下面的代码就容易理解的多。其中是罗列了主要的逻辑处理,省略掉了一些判断和CAS方法 。顺便说一下JUC的源码会大量使用Unsafe这个类。

private static final sun.misc.Unsafe U;

这个类的方法都是底层封装的native方法,用于比较替换的方式来修改值,是线程安全的。

private void externalSubmit(ForkJoinTask<?> task) {
    // r是随机数,此处双重检测,确保r不为0
    int r;                                    // initialize caller's probe
    if ((r = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();
        r = ThreadLocalRandom.getProbe();
    }
    // 死循环
    for (;;) {
        WorkQueue[] ws; WorkQueue q; int rs, m, k;
        // move默认为false
        boolean move = false;
        // 如果runstate小于0 则线程池处于SHUTDOWN状态,配合进行终止
        if ((rs = runState) < 0) {
            // 终止的方法 并抛出异常,拒绝该任务
            tryTerminate(false, false);     // help terminate
            throw new RejectedExecutionException();
        }
        //线程池可用并且workQueues为null表示未初始化的情况下
        else if ((rs & STARTED) == 0 ||     // initialize
                 ((ws = workQueues) == null || (m = ws.length - 1) < 0)) {
            
            //争抢到锁后,创建workQueues数组            
            //实际上这个分支只是创建了外层的workQueues数组,此时数组内的内容还是全部都是空的
            workQueues = new WorkQueue[n]; 
        }
        //workQueues根据随机数取到槽位不为空则证明可以添加
        else if ((q = ws[k = r & m & SQMASK]) != null) {
            //把任务添加到workQueue下面的ForkJoinTask数组内
            U.putOrderedObject(a, j, task);
        }
        // 如果状态不为RSLOCK 上面两个分支都判断过了,说明workQueues数据在这个位置没有工作队列
        else if (((rs = runState) & RSLOCK) == 0) {
            //创建一个新的队列,赋值给ws
            ws[k] = q;
        }
        else
            // 将move改为true
            move = true;                  
        if (move)
            // 重新计算r
            r = ThreadLocalRandom.advanceProbe(r);
    }
}

线程是怎么工作的

前面的介绍了解了ForkJoinPool是如何将任务提交到工作队列的,后续探究的方向是线程是如何创建的(addWorker)线程如何提取任务的(runWorker)。

线程的创建方法在调用链路是signalWork ->  tryAddWorker -> createWorker。在createWorker方法使用构造参数里面的ForkJoinWorkerThreadFactory线程工厂来创建ForkJoinWorkerThread

    private boolean createWorker() {
        ForkJoinWorkerThreadFactory fac = factory;
        Throwable ex = null;
        ForkJoinWorkerThread wt = null;
        try {
            if (fac != null && (wt = fac.newThread(this)) != null) {
                //线程就绪
                wt.start();
                return true;
            }
        } catch (Throwable rex) {
            ex = rex;
        }
        //失败销毁线程
        deregisterWorker(wt, ex);
        return false;
    }

线程工作的runWorker方法在ForkJoinWorkerThread类的run方法中调用。从工作队列获取到ForkJoinTask任务之后最终会执行任务包装的ForkJoinTask。

/**
 * 通过调用线程的run方法
 */
final void runWorker(WorkQueue w) {
    // 初始化队列是否需要扩容
    w.growArray();                   // allocate queue
    int seed = w.hint;               // initially holds randomization hint
    int r = (seed == 0) ? 1 : seed;  // avoid 0 for xorShift
    // 死循环
    for (ForkJoinTask<?> t;;) {
        // 获取任务,工作窃取来自这个方法
        // 这个r是个随机数
        if ((t = scan(w, r)) != null)
            // 运行task
            w.runTask(t);
        //当前线程进行等待
        else if (!awaitWork(w, r))
            break;
        r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
    }
}

需要特殊介绍的是工作窃取处理逻辑来自scan方法,传入参数是当前workerQueue和一个随机数。通过随机数来向后遍历workerQueue数组,判断索引位置的workerQueue是否有任务可以获取。这部分的代码太长,直接找了一位大神写的注释粘过来。

/**
 * 通过scan方法进行任务窃取,扫描从一个随机位置开始,如果出现竞争则通过魔数继续随机移动,反之则线性移动,直到所有队列上的相同校验连续两次出现为空,则说明没有任何任务可以窃取,因此worker会停止窃取,之后重新扫描,如果找到任务则重新激活,否则返回null,扫描工作应该尽可能少的占用内存,以减少对其他扫描线程的干扰。
 *
 * @param w the worker (via its WorkQueue)
 * @param r a random seed
 * @return a task, or null if none found
 */
private ForkJoinTask<?> scan(WorkQueue w, int r) {
    WorkQueue[] ws; int m;
    // 如果workQueues不为空且长度大于1,当前workQueue不为空
    if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
        // ss为扫描状态
        int ss = w.scanState;                     // initially non-negative
        // for循环 这是个死循环  origin将r与m求并,将多余位去除。然后赋值给k
        for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
            WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
            int b, n; long c;
            // 如果k处不为空
            if ((q = ws[k]) != null) {
                // 如果task大于0
                if ((n = (b = q.base) - q.top) < 0 &&
                    (a = q.array) != null) {      // non-empty
                    // 计算i
                    long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                    // 得到i处的task 
                    if ((t = ((ForkJoinTask<?>)
                              U.getObjectVolatile(a, i))) != null &&
                        q.base == b) {
                        // 如果扫描状态大于0
                        if (ss >= 0) {
                            // 更改a中i的值为空 也就是此处将任务窃取走了
                            if (U.compareAndSwapObject(a, i, t, null)) {
                                // 将底部的指针加1
                                q.base = b + 1;
                                // 如果n小于-1 则通知工作线程工作
                                if (n < -1)       // signal others
                                    signalWork(ws, q);
                                // 将窃取的task返回
                                return t;
                            }
                        }
                        // 如果 scan状态小于0 则调用tryRelease方法唤醒哪些wait的worker
                        else if (oldSum == 0 &&   // try to activate
                                 w.scanState < 0)
                            // 调用tryRelease方法 后续详细介绍
                            tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                    }
                    // 如果ss小于0 
                    if (ss < 0)                   // refresh
                        // 更改ss
                        ss = w.scanState;
                    r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
                    origin = k = r & m;           // move and rescan
                    oldSum = checkSum = 0;
                    continue;
                }
                checkSum += b;
            }
            // 此处判断k,k在此通过+1的方式完成对原有workQueues的遍历
            if ((k = (k + 1) & m) == origin) {    // continue until stable
                if ((ss >= 0 || (ss == (ss = w.scanState))) &&
                    oldSum == (oldSum = checkSum)) {
                    if (ss < 0 || w.qlock < 0)    // already inactive
                        break;
                    int ns = ss | INACTIVE;       // try to inactivate
                    long nc = ((SP_MASK & ns) |
                               (UC_MASK & ((c = ctl) - AC_UNIT)));
                    w.stackPred = (int)c;         // hold prev stack top
                    U.putInt(w, QSCANSTATE, ns);
                    if (U.compareAndSwapLong(this, CTL, c, nc))
                        ss = ns;
                    else
                        w.scanState = ss;         // back out
                }
                checkSum = 0;
            }
        }
    }
    return null;
}

至此对于ForkJoinPool的整体流程都已知悉,下面就ForkJoinPool如何实现并行任务进行探究。

3. 任务并行处理

就像ScheduledThreadPoolExecutor执行周期任务需要使用scheduleWithFixedDelay和scheduleAtFixedRate方法。ForkJoinPool执行并行任务也需要特殊的设计,需要依赖RecursiveAction和RecursiveTask。重写里面的compute()方法,在compute()方法中分解任务,并且将任务结果合并。

RecursiveTask代表有返回值的任务
RecursiveAction代表没有返回值的任务。

 示例

    private static class SumRecursiveTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int begin;
        private int end;

        public SumRecursiveTask(long[] numbers, int begin, int end) {
            this.numbers = numbers;
            this.begin = begin;
            this.end = end;
        }

        //对任务进行拆分
        @Override
        protected Long compute() {
            // 把100当作最小粒度的任务
            if (end - begin < 100) {
                long total = 0;
                for (int i = begin; i <= end; i++) {
                    total += numbers[i];
                }
                return total;
            } else {
                //简单处理平分总任务
                int middle = (begin + end) / 2;
                SumRecursiveTask left = new SumRecursiveTask(numbers, begin, middle);
                SumRecursiveTask right = new SumRecursiveTask(numbers, middle + 1, end);
                left.fork();
                right.fork();
                return left.join() + right.join();
            }
        }
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        long[] numbers = LongStream.rangeClosed(1, 100000000).toArray();
        long result = forkJoinPool.invoke(new SumRecursiveTask(numbers, 0, numbers.length - 1));
        System.out.println("返回值结果:"+result);
        System.out.println("线程数:"+forkJoinPool.getPoolSize());
        System.out.println("窃取任务数:"+forkJoinPool.getStealCount());
        forkJoinPool.shutdown();

    }


    ---
    返回值结果:5000000050000000
    线程数:8
    窃取任务数:23

在上面的示例中自定义了继承RecursiveTask的SumRecursiveTask类,通过重写compute来处理任务分配逻辑。其中使用了task.fork方法代表分配任务,task.join方法代表汇总任务结果。这两个方法的源码不太复杂。但是如果在任务切分时的最小粒度控制的不合理,对整体性能会有很大影响

    public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }

    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

4. CompletableFuture

在JDK1.8后广泛使用的CompletableFuture和并行流都是采用了ForkJoin。使用最多的CompletableFuture在开发中常用作任务并行调度和任务编排,提供了许多易用的方法,使用范围很广。但是CompletableFuture使用不当也会造成比较严重的生产事故。

    /**
     * Default executor -- ForkJoinPool.commonPool() unless it cannot
     * support parallelism.
     */
    private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

CompletableFuture默认使用的是ForkJoinPool.commonPool(),核心线程参数是CPU核数-1。也就是说在生产项目中如果大量使用了CompletableFuture而没有自定义线程池。共用的ForkJoinPool.commonPool()如果到了性能瓶颈会响应所有任务的执行性能,这是使用CompletableFuture的常见误区。进而引出我们下面讨论的话题如何正确使用线程池、如何正确设置线程参数。

使用默认线程池主打的就是 : 有福同享 有难同当

二. 如何正确设置线程池参数 

这个问题也算是一个常见的面试题,提到线程池就离不开这个话题。通常的回答可能是这样的:

  • CPU密集型:corePoolSize = CPU核数 + 1
  • IO密集型:corePoolSize = CPU核数 * 2

但是这个设置思路只能是个理论值,对于实际来讲每个项目在实际运行中涉及到跟性能相关的因素不同导致最终符合项目的实际参数也会不同。比较好的做法是根据经验和压测结果来设置一个基础值,经过压测之后的参数是满足绝大多数使用场景的。对于一些个别的场景比如流量激增的场景,需要实现能不停机动态更新的能力

在ThreadPoolExecutor的7个初始化参数中,影响任务运行的只有3个关键参数:corePoolSize、maximumPoolSize,workQueue的长度。所以在动态修改参数时我们只需要修改这3个值就能提升线程池处理任务的能力。对于这部分JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法:

 但是唯独少了workQueue的队列长度的设置。原因是比如我们常用的LinkedBlockingQueue队列它的长度变量是final定义的不可变字段。

    /**
     * 阻塞队列的容量,默认为Integer.MAX_VALUE
     */
    private final int capacity;

所以我们要想实现对缓存队列的修改需要修改LinkedBlockingQueue源码,可以新建一个自定义队列把LinkedBlockingQueue的代码复制获取。只去掉capacity字段的final关键字和增加setCapacity的方法。

    public void setCapacity(int capacity) {
        final int oldCapacity = this.capacity;
        this.capacity = capacity;
        //获取当前队列的容量
        final int size = count.get();
        if (capacity > size && size >= oldCapacity) {
            //代表修改前队列满了
            //修改后增大了队列长度可以继续添加
            signalNotFull();
        }
    }

1. 动态修改线程池的实现 

下面以一个实际的示例来展示如何动态修改线程池参数。在spring项目中创建一个ThreadPoolExecutor的Bean,然后通过调用接口对这个Bean进行修改。在实际项目中也是大致的思路只不过触发的方式会有不同。

@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor versionThreadPoolExecutor(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                //核心线程数
                2
                //允许的最大线程数
                , 4
                //线程空间时间
                , 60
                //时间单位-秒
                , TimeUnit.SECONDS
                //指定自定义的缓存队列长度为5
                , new VersionLinkedBlockingQueue<>(5)
                //线程工厂
                , new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("version-demo-pool-" + threadNumber.getAndIncrement());
                return thread;
            }
        }
                //自定义拒绝策略
                , new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                //实际项目中是不会抛弃任务的
                //出现拒绝策略说明线程池的参数已经不满足业务场景了需要做参数修改。
                //同时对于超出执行的能力的任务
                //可以选择使用其他线程执行或使用消息队列或者存入数据库等待定时任务扫描
                System.out.println("线程池拒绝策略");
            }
        });
        return executor;
    }
}
    @Resource
    private ThreadPoolExecutor versionThreadPoolExecutor;

    @PostMapping("/onChange")
    public void modifyPool(){
        System.out.println(String.format("修改前 -- 当前核心线程数 %s,当队列使用长度%s,当前队列剩余容量%s", versionThreadPoolExecutor.getCorePoolSize(),versionThreadPoolExecutor.getQueue().size(),versionThreadPoolExecutor.getQueue().remainingCapacity()));
        versionThreadPoolExecutor.setCorePoolSize(3);
        VersionLinkedBlockingQueue<Runnable> queue = (VersionLinkedBlockingQueue)versionThreadPoolExecutor.getQueue();
        queue.setCapacity(10);
        System.out.println(String.format("修改后 -- 当前核心线程数 %s,当队列使用长度%s,当前队列剩余容量%s", versionThreadPoolExecutor.getCorePoolSize(),versionThreadPoolExecutor.getQueue().size(),versionThreadPoolExecutor.getQueue().remainingCapacity()));
    }

上面的触发方式只是一个简单示例,在实际项目中需要稍加改造。正式环境都是集群部署的,会有多台机器没办法通过一个接口修改所有机器的线程池的线程配置。 正确的做法是可以通过集成配置中心(Nacos、Apollo)来实现触发。针对这部分内容可以查看一个开源案例:首页 | dynamictp

2. 线程池监控

前文说明了如何动态的修改线程池参数,但是什么时候修改又是个问题。也就是说怎么判定需要对线程池进行优化,为了对线程资源的有效管理,对线程池配置监控和告警是十分有必要的。获取线程池运行状态可以通过JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法。

现在有了对线程池的数据收集的方法,但对线程池监控我们想要实现的效果一定时对每一个线程池进行监控。有一个方法可以在Spring项目内维护一个集合来存储所有的ThreadPoolExecutor的bean。然后通过定时任务线程每隔一段时间汇总数据并上报。

@Component
public class ThreadPoolExecutorBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ThreadPoolExecutor) {
            //注册线程的BeanName
            ThreadPoolExecutorFactory.registerExecutor(beanName);
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
public class ThreadPoolExecutorFactory {

    private static final Set<String> EXECUTOR_REGISTRY = Collections.synchronizedSet(new HashSet<String>());

    public static void registerExecutor(String beanName) {
        EXECUTOR_REGISTRY.add(beanName);
    }

    public static Set<String> getAllExecutorNames() {
        return EXECUTOR_REGISTRY;
    }
}
@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor versionThreadPoolExecutor(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                //核心线程数
                2
                //允许的最大线程数
                , 4
                //线程空间时间
                , 60
                //时间单位-秒
                , TimeUnit.SECONDS
                //指定自定义的缓存队列长度为5
                , new VersionLinkedBlockingQueue<>(5)
                //线程工厂
                , new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("version-demo-pool-" + threadNumber.getAndIncrement());
                return thread;
            }
        }
                //自定义拒绝策略
                , new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                //实际项目中是不会抛弃任务的
                //出现拒绝策略说明线程池的参数已经不满足业务场景了需要做参数修改。
                //同时对于超出执行的能力的任务
                //可以选择使用其他线程执行或使用消息队列或者存入数据库等待定时任务扫描
                System.out.println("线程池拒绝策略");
            }
        });
        return executor;
    }
}
@Component
public class ThreadPoolExecutorMonitor implements ApplicationContextAware {

    private static ScheduledExecutorService MONITOR_EXECUTOR = new ScheduledThreadPoolExecutor(1);

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = context;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过id名获取bean
    public <T> T getBean(String name) {
        return (T) getApplicationContext().getBean(name);
    }

    @PostConstruct
    public void init() {
        MONITOR_EXECUTOR.scheduleWithFixedDelay(() -> {
            Set<String> allExecutorNames = ThreadPoolExecutorFactory.getAllExecutorNames();
            allExecutorNames.forEach((beanName) -> {
                ThreadPoolExecutor pool = getBean(beanName);
                System.out.println(String.format("监控数据上报 -- 线程名称%s 当前核心线程数 %s,当队列使用长度%s,当前队列剩余容量%s", beanName, pool.getCorePoolSize(), pool.getQueue().size(), pool.getQueue().remainingCapacity()));
            });
        }, 20, 5, TimeUnit.SECONDS);
    }
}

上面的代码就是对线程池监控的简单实现,大致思路就是这个流程,要是作用在线上环境一些细节还需要完善。

三. 总结 

经过这几天的努力终于把线程池相关的点介绍清楚了,时间有些匆忙有些细节之处没有讲透彻。但对于大体流程是通顺的。其中涉及到了大量的源码,可能会有些枯燥。但是耐心看完就会有所收获,无他唯手熟尔。对于线程池的总结只有一句话:合理参数、线程隔离。在正式环境的核心功能设置线程池参数最好通过压测来设置,同时不同业务之间要线程隔离。避免非核心业务占用核心业务的资源。

下一篇计划开设一篇关于Spring的实践类的文章,总看源码我也懵啊,换个轻松点的。

最后感谢大家观看!如有帮助 感谢支持!

后续争取每周一更

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

实操手

如有帮助 感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值