图解线程池——清新脱俗的讲原理

网上介绍线程池的文章很多,质量好坏不一。能讲的很透彻的,确实不多。

本人能力有限,本文先从原理入手,讲清楚线程池是怎么运行的。

至于源码的分析,将单独写一篇(《线程池源码详解》)。

全文以 java 1.8 来说明。

一、示例代码

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        for(int i =1; i <= 10; i++){
            int index = i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    String currentThreadName = Thread.currentThread().getName();
                    log.info("第{}次任务结束,执行者:{}", index, currentThreadName);
                }
            });
        }
        pool.shutdown();
        System.out.println("All thread is over");
    }

这个小例子中,执行了10次 run() 方法,但其实只起了两个线程。

这就是线程池的一个特点:节省资源

线程的创建与销毁是非常耗能的操作,若执行一个任务就起一个线程,那是消耗大了去了。

二、线程池的创建

示例中,Executors.newFixedThreadPool(2) ,最终是执行的下面这个方法(是ThreadPoolExecutor这个类中的方法)

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

这里面有7个参数,等介绍源码的时候再详细说。这里只说涉及本本篇的三个:

  • corePoolSize 核心线程数
  • maximumPoolSize 最大线程数
  • workQueue 阻塞队列

顺便说一句,在线程池里,没有什么所谓的核心线程、非核心线程

有些文章里说核心线程怎么怎么样非核心线程怎么怎么样——瞎掰!

线程池创建出来的线程,地位是一样的,功能也是一样的。

阻塞队列,之前的博客有详细的讲解,这里就再展开。

线程的执行

execute(Runnable command) 这个方法是核心方法。


public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) { // 1、工作线程数量小于核心线程数,创建线程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { // 2、将任务放入阻塞队列
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 3、队满时,继续创建线程,直至工作线程数量达到最大值。
            reject(command); // 4、执行拒绝策略
    }

我写了4个注释,这是execute方法的执行逻辑。这里我举个通俗的例子,来说明。

假如病毒流行,感染的病人被领到医院看病。这里的病人相当于需要执行的任务

医院里总共有 6 套超级防护服,医生需穿上防护服才可以给病人看病。

医院里拿出4套防护服供医生使用,留下两套备用。

这里的 6 相当于 maximumPoolSize 最大线程数,4 相当于corePoolSize

如果医生都在忙,那病人就被安置到大厅,排队看医生。这里的大厅相当于workQueue

在这里插入图片描述
上面是一个空图,现在来了一个病人,医生穿上防护服,给病人看病。

代码层面,就是执行 addWorker(command, true) 这一行。
在这里插入图片描述
之后又来俩病人。那就继续创建线程,同时呢,假设第一个来的病人,已经看完了,离开了诊疗室。

在这里插入图片描述
现在呢,假如一下子来了三个病人,

那先新上一个医生看病,第二、三个病人到大厅等着。

其实空闲的那个医生,会到大厅里叫病人的

顺便说一句:当未达到核心线程数时,就先创建线程,不管其它线程是不是闲着。

在这里插入图片描述
之后再来的病人,就进大厅等着。相当于执行这行代码 workQueue.offer(command)

顺便说一句:医生不会闲着,看完一个,就从大厅叫一个进来。

如果病人来的又多又快,四个医生都没闲着,大厅也满了,那就把备用的两个防护服给用上。

相当于执行这行代码 addWorker(command, false)

在这里插入图片描述
现在是六个医生全力工作,看完一个病人,就从大厅领进来一个病人处理。

如果即使是这样,病人来是源源不断的来医院,这时就进行拒绝策略。

相当于执行这行代码 reject(command)

循环取任务

addWorker(Runnable firstTask, boolean core) 这个方法是创建线程并会启动线程。

最终会调用 Thread 类中 start() 方法。JVM 会自在合适的时候,调用 线程的 run() 方法。

如果面试官问——JVM 怎么调用 run 方法的,机制是什么,合适的时候是什么时候

我不知道,已经脱离java源码的范畴了,你想知道,自己看hotSpot 的源码吧

ThreadPoolExecutor 类中,重写了 run() 方法


        public void run() {
            runWorker(this);
        }

runWork() 这个方法,简化如下。

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); 
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                try {
                      task.run();
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            
        } finally {
             processWorkerExit(w, completedAbruptly);
        }
    }

这个方法中, task.run() 会调用示例代码中 重写的那个run() 方法,也就是执行任务了。

仔细看,任务是从 getTask() 方法中取出来的,是循环执行,while 循环

前面例子中,医生看完一个病人,会从大厅里叫一个新病人。

当一个医生从大厅里叫病人,比如叫了两分钟,都没叫到。

那就认为没病人了,会考虑要不要脱了防护服,回去休息。

要不要撤的依据是,诊疗室里的医生有几个,即与 corePoolSize 比较

如果大于4个,那就可以回去消息,否则就在诊疗室待命。

在这里插入图片描述
也就是说,没有病人的时候,会蜕变成上图的样子,直到一个病人与没有了,四个医生在待命。

取任务的方法,代码简化如下

private Runnable getTask() {
        for (;;) {          
            int wc = workerCountOf(c);
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
            } catch (InterruptedException retry) {
            }
        }
    }

这个方法本质是从队列中取任务,取出来后,在 runWork() 方法里执行。

那最忙的时候,是六个医生在工作,之后不忙了,缩减到4个,代码层面体现在哪里呢?

runWork() 方法,在finally里执行 processWorkerExit() 这个方法。

能走到这个方法,意味着大厅里没有病人了,可以会缩减医生。

简化后的代码如下:


private void processWorkerExit(Worker w, boolean completedAbruptly) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w); // 缩减线程
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    

workers.remove(w) 这个就是将工作线程给剔除,GC回收。

线程关闭

在示例代码中,调用 shutdown() 方法,会关闭线程。代码如下,这个不用简化,很简洁

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess(); // 检查有没有权限关闭线程池
            advanceRunState(SHUTDOWN); // 更改线程池的状态
            interruptIdleWorkers(); // 中断线程
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

等到介绍源码的时候,再细说如何优雅的关闭线程池。

总结

梳理下,本篇简单介绍了,线程池的创建、运行、关闭。

用画图的方式,介绍了,线程池工作原理,即 execute() 方法的四大步

  1. 接到任务,先创建工作线程
  2. 工作线程达到一定数量,把任务往阻塞队列放
  3. 阻塞队列放不下了,创建工作线程(不超过最大线程数)
  4. 如果前三步应付不过来,执行拒绝策略

线程工作,即 addWorker() 方法执行里,调用 Thread 类的 start() 方法,JVM 会调用 run() 方法

重写的 run() 方法执行 runWorker() 方法,此方法会循环取任务,直到没有任务了。

随后会维护线程池里线程的数量。最后是关闭线程池。

看了之后,有没有一种错觉,线程池的代码挺简单的呀,没有什么复杂的逻辑呀!

事实是代码巨复杂,这篇文章是把代码简化了,只说大流程。

有了这篇的基础,下篇解析源码时,会详细说明

  • 线程池状态是怎么控制的
  • 创建线程是怎么防止并发的
  • 核心线程数和最大线程数,具体干啥的
  • 哪些地方用了 AQS 框架
  • 线程最长存活时间,是怎样控制线程生命周期的
  • addWorker 时,传一个空任务,是干嘛的
  • 常用线程池,是怎么选阻塞队列的
  • ……
    所有这些,请看下篇《线程池源码详解》。
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值