Java线程池到底是怎么运行的?(二)

a22ba9ffe14201a5c26591ee3e21ab6a.png

前言

上篇文章,我们讲解了Java线程池的正确使用方法以及线程池运行基本原理,本篇文章我们深入到源码视角来验证我们的运行基本原理是否如我们所想的那样。

一、源码分析

1.1 ThreadPoolExecutor的类图结构如下:

1f13309de7b3eb5bc25c14f721899c5a.png

Executor: 仅提供了execute()一个执行任务的通用接口,使得任务提交与线程调度、执行细节分离。具体实现交由ThreadPoolExecutor类去实现的。

ExecutorService: 它提供了更丰富的线程管理和控制功能。其中shutdown()定义为关闭线程池,不在接收新任务的提交,当前已接收的任务会继续执行;submit()定义为提交任务的接口,并且会等待任务执行的结果,而Executor提供的execute()是没有返回结果的。

AbstractExecutorService: 对ExecutorService定义的submit()的实现

ThreadPoolExecutor: 负责任务的接收、暂存、任务处理、线程池管理的具体实现。

1.2 线程池运行状态&活跃线程数

public class ThreadPoolExecutor extends AbstractExecutorService {
     // 静态变量COUNT_BITS=29
     private static final int COUNT_BITS = Integer.SIZE - 3; 

     // 1向左移动29位,然后-1, CAPACITY的二进制表示:0000 1111 1111 1111 1111 1111 1111 1111
     private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // 线程池状态为RUNNING,-1向左移动29位
    // 二进制表示:1110 0000 0000 0000 0000 0000
    private static final int RUNNING    = -1 << COUNT_BITS;

    // 线程池状态为SHUTDOWN,0向左移动29位
    // 二进制表示:0000 0000 0000 0000 0000 0000 0000 0000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;

    // 线程池状态为STOP,1向左移动29位
    // 二进制表示:0010 0000 0000 0000 0000 0000 0000 0000
    private static final int STOP       =  1 << COUNT_BITS;

    // 线程池状态为TIDYING,2向左移动29位
    // 二进制表示:0100 0000 0000 0000 0000 0000 0000 0000
    private static final int TIDYING    =  2 << COUNT_BITS;

    // 线程池状态为TERMINATED,3向左移动29位
    // 二进制表示:0110 0000 0000 0000 0000 0000 0000 0000
    private static final int TERMINATED =  3 << COUNT_BITS;

    // 1110 0000 0000 0000 0000 0000 0000 0000
    // 0000 0000 0000 0000 0000 0000 0000 0000  或运行
    // ----------------------------------------------
    // 1110 0000 0000 0000 0000 0000 0000 0000
    
    // 获取线程池的运行状态,通过一下运算可以看出
    // 最终只保留了高3位的信息,而高3为恰好存储线程池的运行状态值
    // CAPACITY:  0000 1111 1111 1111 1111 1111 1111 1111
    // ~CAPACITY: 0000 0000 0000 0000 0000 0000 0000 0000 非运算
    // c:         1110 0000 0000 0000 0000 0000 0000 0000 与运算
    // -------------------------------------------------------
    //           1110 0000 0000 0000 0000 0000 0000 0000 
    private static int runStateOf(int c)     { return c & ~CAPACITY; }

    // 获取线程池活跃线程数,通过运算可以看出
    // 高3位信息被抹除了,最终只保留了低29位的信息,
    // 所以低29位存储的是线程池的活跃线程数
    // CAPACITY:  0000 1111 1111 1111 1111 1111 1111 1111
    // c:         1110 0000 0000 0000 0000 0000 0000 0000 与运算
    // -------------------------------------------------------
    //            0000 0000 0000 0000 0000 0000 0000 0000
    private static int workerCountOf(int c)  { return c & CAPACITY; }

    // 获取线程池运行状态和活跃线程数两个信息,使用或运算,同时保留了高3位和低29位的信息
    // rs(线程池运行状态): 1110 0000 0000 0000 0000 0000 0000 0000
    // wc(活跃线程数):    1110 0000 0000 0000 0000 0000 0000 0000 或运算
    // -------------------------------------------------------
    //                  1110 0000 0000 0000 0000 0000 0000 0000
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    // 通过运算可以得出,线程池初始状态即高3为全为1,表示运行中状态
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
}

线程池运行状态和活跃线程池使用AtomicInteger定义,有几个好处:

  1. AtomicInteger作为并发修改安全的工具类,其实现原理是依靠CAS操作,从而避免使用效率相对较低的synchroized(其实synchroized的底层性能优化的已经非常好了,比如jdk15废弃了偏向锁)来保证

  2. AtomicInteger在硬件层面是用了cmpxchg函数来实现,本质通过硬件源语来避免并发问题

  3. AtomicInteger内部变量使用了volatile关键字修饰,从而保证了内存可见性

抛个题外话:CAS并不是万能钥匙,比如它本身也存在一些问题,比如ABA问题等;再比如由volatile带出的一系列知识点,比如原子性、内存可见性、指令重排等等, 这里暂且先挖个坑,在后续的知识串联过程中,我们逐渐深入

1.3 任务提交模块

任务提交的两种方式,示例代码如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 10, 60, TimeUnit.SECONDS, 100);
// 方案1: 使用execute提交任务, 没有返回值
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用execute提交任务...");
    }
});

// 方案2: 使用submit提交任务, 有返回值,最终底层还是调用了execute方法
Future<?> futureResult = threadPoolExecutor.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用submit提交任务...");
    }
});

点进去提交任务的方法内部发现,最终任务提交都走了execute方法,所以我们来分析下execute内部实现

public void execute(Runnable command) {
    // Step1: 提交的任务为null,直接抛出
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    // Stpw2: 获取当前活跃的线程数是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
     // Step2.1: 将任务和线程包装成一个Worker,完成后由该Worker去执行任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // Step3: 若当先线程池是RUNNING状态 && 阻塞队列成功接收了提交的任务
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // Step3.1: 二次检查线程池运行状态已不是RUNNING,
        // 则将刚添加到阻塞队列的任务移除掉,执行成功后,
        // 将提交的任务执行具体的拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // Step3.2: 若活跃线程数为0,则包装出一个任务为null的Worker线程,
        // 然后从阻塞队列中获取任务去处理 
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // Step4: 则尝试将任务提交给线程池,此时线程池有可能是RUNNING状态,
    // 只是阻塞队列添加任务失败了,尝试启动一个非核心线程去执行任务,若提交失败,则执行具体的拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        // Step1: 获取线程池运行状态
        int rs = runStateOf(c);

        // Step2: 主要工作是检查线程池运行状态,如果是非RUNNING状态,就不会在创建新Worker工作线程出来了。
        // 大白话翻译一下:
        // 1). 当线程池运行状态为SHUTDOWN时:
        //  a). 提交了新任务进来,则增加Worker失败
        //  b). 没有提交新任务,阻塞队列也没有待处理的任务,则增加Worker失败
        //      (也就是说,当阻塞队列中还有任务,则需要新建Worker来处理任务)
        // 2). 当线程池运行状态为 STOP | TIDYING | TERMINATED 时,则添加Worker失败
        //     (也就是说,当为以上三种状态时,就不会在新建Worker来处理任务了)
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        // Step3: 主要工作就是活跃线程数+1,通过死循环+CAS直到活跃线程数增加成功
        //        或者当活跃线程数达到最大值,则不会在新建Worker
        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
     // Step4: 将提交的任务和新建的线程包装成一个Worker对象
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
         // Step4.1: 使用ReentrantLock锁,使得同一时刻只有一个Worker可以成功加入到Worker集合中
         //          这个Worker集合是用HashSet进行存储的。
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive())
                        throw new IllegalThreadStateException();
                    // 将Worker添加到HashSet里面
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // Step4.2: 当Worker成功添加到HashSet里面后,就启动线程开始处理提交的任务
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
     // Step5: 如果启动线程失败了,则把添加到HashSet里面的Worker移除掉,活跃线程数-1
     //        尝试将线程池状态流转到TERMINATED
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWork()的主要工作其实就是创建工作线程,然后执行提交的任务,注意:当新建出来的Worker会直接处理当前 提交的任务,处理完成后才会去阻塞队列中获取任务继续执行(不管是创建核心线程还是非核心线程)

在该方法里面出现了一个新的知识点,ReentrantLock锁,锁定局部代码以提高Worker创建效率,相信大家都知 道ReentrantLock,其继承了大名鼎鼎的AQS,这里也先挖个坑,后面我们会专门针对这个知识点展开介绍。

1.4 任务执行模块

final void runWorker(Worker w) {
 Thread wt = Thread.currentThread();
 // Step1: 从Worker中获取待处理的任务
 Runnable task = w.firstTask;
 w.firstTask = null;
 // Step2: 释放当前Worker持有的锁
 // 内部代码如下,释放Worker的锁持有者,将状态设置为0
 // 其实就是AQS的三板斧,持有锁的线程,锁状态,AQS队列,后续在分析AQS时在详细展开
 // protected boolean tryRelease(int unused) {
 //    setExclusiveOwnerThread(null);
 //    setState(0);
 //    return true;
 // }
 w.unlock();
 boolean completedAbruptly = true;
 try {
    // Step3: 通过while循环,执行创建Worker时绑定的任务 
    // || 通过getTask()从阻塞队列获取任务
     while (task != null || (task = getTask()) != null) {
      // Step3.1: 对当前Worker加锁
         w.lock();
         // Step3.2: 获取线程池运行状态,当状态为STOP | TIDYING | TERMINATED时
         //          或者当前线程已中断 && 当状态为STOP | TIDYING | TERMINATED 
         //          && 当前Worker线程未中断时,
         //          对当前Worker执行中断操作
         if ((runStateAtLeast(ctl.get(), STOP) ||
              (Thread.interrupted() &&
               runStateAtLeast(ctl.get(), STOP))) &&
             !wt.isInterrupted())
             wt.interrupt();

         try {
          // Step3.3: 在任务开始正式执行前留出一个口子,方便业务定制开发,比如监控数据采集等
             beforeExecute(wt, task);
             Throwable thrown = null;
             try {
              // Step3.4: 启动任务执行
                 task.run();
             } catch (RuntimeException x) {
                 thrown = x; throw x;
             } catch (Error x) {
                 thrown = x; throw x;
             } catch (Throwable x) {
                 thrown = x; throw new Error(x);
             } finally {
              // Step3.5: 在任务执行后流出一个口子,方便业务定制开发
                 afterExecute(task, thrown);
             }
         } finally {
             task = null;
             w.completedTasks++;
             // Step3.6: 释放当前Worker的锁
             w.unlock();
         }
     }
     completedAbruptly = false;
 } finally {
     // Step4: 任务执行完成后,操作Worker线程退出
     processWorkerExit(w, completedAbruptly);
 }
}

1.5 Worker线程的回收

final void tryTerminate() {
 for (;;) {
     int c = ctl.get();
     // Step1: 检查线程池运行状态。当为RUNNING状态 || 当前状态为TIDYING 
     //        || (SHUTDOWN状态 && 队列不为空),
     //        则不会流转到 TERMINATED 状态
     if (isRunning(c) ||
         runStateAtLeast(c, TIDYING) ||
         (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
         return;

     // Step2: 当活跃线程数不为0时,将非活跃线程中断
     if (workerCountOf(c) != 0) {
         interruptIdleWorkers(ONLY_ONE);
         return;
     }

     final ReentrantLock mainLock = this.mainLock;
     mainLock.lock();
     try {
         // Step3: 将线程池状态通过CAS设置为TIDYING
         if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
             try {
              // Step4: 内部无没有做任何,预留处理的口子,供业务自定义使用
                 terminated();
             } finally {
                 // Step5: 将线程池状态通过CAS设置为TERMINATED
                 ctl.set(ctlOf(TERMINATED, 0));
                 // Step6: 唤醒所有在等待队列中的线程
                 termination.signalAll();
             }
             return;
         }
     } finally {
         mainLock.unlock();
     }
     // else retry on failed CAS
 }
}

2. 后续

从最简单的如何创建线程池开始,深入到生成环境如何正确使用线程池,然后再深入到线程池 的基本工作原理,线程池生命周期,最后深入到源码分析,完整了解了ThreadPoolExecutor 的运行机制。

在进行源码分析时,我们发现了三个重要的技术支撑点来完成线程池的完整运行,

  1. ReentrantLock在Worker创建、任务执行、线程池销毁等全生命周期中扮演的锁的角色

  2. AbstractQueuedSynchronizer在Worker执行任务过程中,加锁逻辑在Worker类重写了tryAcquire()方法

  3. AtomicInteger维护的线程池运行状态和活跃线程数 实现为不可重入,而ReentrantLook不管是公平锁还是非公平锁,加锁逻辑都是可重入的。

后续文章我们会继续围绕这三个基础知识点,进一步展开分析其用法和底层实现原理。

 做一个有深度的技术人

Java运行GroovyShell时,可以配置线程池的大小以提高程序的性能和效率。线程池的大小决定了可以同时执行的线程数量,过大的线程池会导致资源浪费,而过小的线程池则可能导致线程阻塞和程序运行缓慢。 根据经验和实践,以下是一些建议的线程池大小配置: 1. 根据CPU核心数进行配置:可以根据当前计算机的CPU核心数来配置线程池大小。一般来说,将线程池的大小设置为CPU核心数的2倍或4倍是一个合理的选择。 2. 考虑任务的类型和复杂性:如果要执行任务是密集计算型的,那么可以增大线程池的大小以充分利用系统资源。而如果任务是I/O密集型的,那么较小的线程池大小可能更为适当,因为线程可能在等待I/O操作完成时被阻塞。 3. 考虑内存和系统资源:大型的线程池会消耗更多的内存和系统资源,因此在配置线程池大小时需要考虑服务器的内存和系统资源限制。确保线程池的大小不会超过系统所能够承受的范围。 4. 考虑任务的排队情况:线程池的大小也要考虑任务的排队情况。如果任务队列中的任务很多,可以适当增大线程池的大小以加快处理速度。而如果任务队列中的任务相对较少,可以减小线程池的大小以节省资源。 综上所述,对于Java运行GroovyShell时的线程池大小配置建议是根据CPU核心数选择2倍或4倍作为初始线程池大小,并根据任务类型、内存和系统资源以及任务排队情况进行适当调整。这样可以在充分利用系统资源的同时,避免出现资源浪费和性能瓶颈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值