Java线程池(二)—— 实现原理

405 阅读7分钟

Java线程池原理

一、前言

之前一篇文章分享了Java线程池的基本使用方式和核心参数的意义,用Executors中的几种线程池初始化参数作为模版,详细解释了这些核心参数的意义。对于初学线程池的人来说,了解这些核心参数的意义是非常重要的,因为这些参数可以说是线程池的灵魂,也是学会自定义线程池必须要了解的东西。附上链接: Java线程池基础

二、Java线程池原理

要说Java线程池的原理首先要明白Java线程池是做什么用的,可以将Java线程池理解为一个线程管理的API,线程池与Kotlin中的协程其实有部分功能是非常相似的,只不过Java的线程池无法实现自动切换线程罢了,它的核心功能是管理线程、复用线程,降低因频繁创建线程而带来的性能损耗。所以在分析线程池的原理时重点应该放在线程池是如何管理线程、复用线程上面。

2.1 初始化

先从初始化流程入手,这里使用JDK中CacheThreadPool为例,它调用的初始化方法是这样的:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}

可以看到其实底层实现类是ThreadPool,跟进代码,最终调用的构造器如下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
    // 可重入锁,线程池内部保证了线程安全,由可重入锁实现
    this.mainLock = new ReentrantLock();
    this.workers = new HashSet();
    this.termination = this.mainLock.newCondition();
    // 检查参数是否合法
    if (corePoolSize >= 0 && maximumPoolSize > 0 && maximumPoolSize >= corePoolSize && keepAliveTime >= 0L) {
        // 检查参数是否合法
        if (workQueue != null && threadFactory != null && handler != null) {
            // 初始化线程池核心变量
            this.corePoolSize = corePoolSize;                  // 核心池大小
            this.maximumPoolSize = maximumPoolSize;            // 线程池最大容量
            this.workQueue = workQueue;                        // 阻塞队列
            this.keepAliveTime = unit.toNanos(keepAliveTime);  // 非核心线程存活时间
            this.threadFactory = threadFactory;                // 线程工厂
            this.handler = handler;                            // 拒绝策略
        } else {
            throw new NullPointerException();
        }
    } else {
        throw new IllegalArgumentException();
    }
}

都是熟悉的参数,线程池的构造器实现还是很简单的,可以看到就是初始化了一些核心参数,没有什么逻辑操作,初始化流程结束了,只要熟悉线程池的核心参数,这个过程看起来就很简单了。

2.2 运行任务

运行任务是分析线程池原理最重要的部分,使用线程池就是要让它执行任务,所以线程池的所有核心逻辑代码的起点就是用户提交一个任务,用户提交任务最简单的代码如下:

ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> {
    // 要执行的逻辑代码
});

提交任务是通过ThreadPool的execute()方法发起的,先来看看这个方法的逻辑:

public void execute(Runnable command) {
    // 参数检查
    if (command == null) {
        throw new NullPointerException();
    } else {
        int c = this.ctl.get();
        if (workerCountOf(c) < this.corePoolSize) {
            // 当前正在运行的线程数小于核心大小,直接创建核心线程运行该任务
            if (this.addWorker(command, true)) {
                return;
            }

            c = this.ctl.get();
        }
        
        // 尝试添加到阻塞队列中
        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable)null, false);
            }
        } else if (!this.addWorker(command, false)) { // 尝试创建非核心线程运行
            // 无法创建非核心线程,线程池已满,触发拒绝策略
            this.reject(command);
        }

    }
}

这个方法的内部逻辑很符合之前对核心参数的解释,根据这个方法的执行逻辑可以推断出线程池创建线程的基本流程:

  1. 判断正在运行的任务数是否小于核心线程数,如果小于核心线程数那么直接创建一个核心线程运行这个任务。

  2. 如果目前正在运行的任务数大于核心线程数,那么尝试添加到阻塞队列中等待调度运行。

  3. 如果阻塞队列已满无法添加到阻塞队列中,那么尝试创建非核心线程运行,如果创建非核心线程失败(线程池容量已满),那么会触发拒绝策略。

以上代码中可以发现,不论是创建核心线程还是非核心线程都是通过Thread.addWorker(Runnable,boolean)方法实现的:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        // 记录了线程池中运行的线程数量和线程的状态,高3位为状态,低29位为线程数
        int c = ctl.get();
        // 线程运行状态
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            // 线程数
            int wc = workerCountOf(c);
            // 检查是否超出线程池的容量
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 将记录线程数的变量+1
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            // 如果没有成功创建,那么将会重试
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    // 初始化线程状态
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        /*
         * 创建一个新的线程,此处worker实际是线程池
         * 为了方便调度/获取线程状态而创建的封装类,
         * 其实worker可以认为是一个线程。这里的线程
         * 是Worker内部实例化时自动创建的。
         */
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 进入临界区之前加上可重入锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
                
                // 状态检查
                if (rs < SHUTDOWN ||
                   (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    
                    // 添加存储Worker的集合中方便后续管理
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 成功加入后启动线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

代码很多,但是逻辑很简单,就是进行了线程池内线程数量的检查和线程运行状态检查,对于核心线程来说只要当前运行的线程数 < 核心线程数,那么可以直接创建核心线程并运行;对于非核心线程来说如果当前运行的线程数 < 线程池容量,那么可以直接创建非核心线程并运行。
需要注意的一点是,线程池内的某个线程是没有核心线程与非核心线程的区别的,所谓的核心线程资源不被回收的意思是线程池内永远会保证有N(核心线程数)个线程没有被释放掉。

2.3 实现线程复用

通过上面的代码了解到,线程池直接调度的资源并不是线程,而是Worker类:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
    
    // 线程池中实际运行的线程
    final Thread thread;
    // 用户提交的任务
    Runnable firstTask;
    
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        // 利用线程工厂创建线程
        this.thread = getThreadFactory().newThread(this);
    }
    
    // 运行用户提交的任务
    public void run() {
        runWorker(this);
    }
{

这里的设计很巧妙,Worker持有的线程对象的Runnable实例其实就是自己,所以当运行Worker中的Thread时调用的是Worker的run方法,下面来看一下runWorker方法的具体实现:

final void runWorker(Worker w) {
    // 这个线程就是上面初始化完成Worker后启动的线程
    Thread wt = Thread.currentThread();
    // Worker中要执行的任务
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 保证线程池停止后所有进程都要中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                (Thread.interrupted() &&
                 runStateAtLeast(ctl.get(), STOP))) &&
                 !wt.isInterrupted())
                 wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行任务
                    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 {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

可以看到,当getTask()方法不为空的时会在这个线程中循环去阻塞队列中取可运行的任务,阻塞队列中为空时会阻塞这个请求直到队列中有数据,所以这个方法只会被阻塞不会为空,这样就达到了线程复用的目的。

三、总结

Java线程池的初始化只是为核心参数赋值,没有其他逻辑;核心逻辑是从用户提交一个任务开始。线程池管理线程是通过核心池大小、线程池最大容量、阻塞队列之间的协作实现的,这里要注意的是:核心线程和非核心线程只是一个概念上的区分,实际上它们是没有任何区别的,从线程池中线程的创建流程可以看出来,它们的创建流程是一模一样的,只是它们在创建时会根据不同的阈值来决定是否要立即创建线程。
线程复用的原理是在runWorker(Worker)方法中实现的,主要原理是在开启Worker中创建的线程后,即使当前任务执行完毕只要还有任务可以被执行那么当前线程就不会被杀死,如此达到线程复用的目的。