Java并发编程 -- 超通俗易懂的线程池源码分析

2019-11-05  本文已影响0人  XinAnzzZ

一、概述

笔者在网上看了好多的关于线程池原理、源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写的太简单,只写了一点皮毛,要么就是是晦涩难懂,看完之后几乎都是一知半解。我想要么是笔者智商捉急,要么就是那些写博客的人以为我很懂所以就大概讲了讲,再或者是作者压根就没认真去讲述线程池。当然多线程以及并发这一块的知识点本身就比较晦涩难懂,但是也不至于找不到一篇文章解惑。于是笔者就下定决心,自己去网上收集资料,自己去买书看那些大神的讲解,然后收集百家之所长,整理一篇不仅适合初学者学习,还适合让老鸟查漏补缺的史上最通俗易懂的线程池知识相关的文章。

写在最前

在写文章之前我的大纲里面是有一节实战的,但是最后还是选择了删掉,不是笔者写不下去了,而是实在是文字有点太多了,我怕读者看到文章这么长望而生畏,所以删掉。又怕自己的文笔太差而让读者产生某些误解,所以很多东西都在重复的去说,以至于随便写了写就已经小一万字了,这里和各位读者说声抱歉。笔者写文章的理念就是精简、明确、表达清晰,但是这部分内容实在不太好分开来写,实际上也没有特别多的内容,而且每一小部分我都有相应的总结,如果认真看,看懂应该没问题。最后,还是希望读者能够耐心看完,能够有所收获。

我知道还有有很多人没有那么多耐心,看不到最后,那么你们就先看总结吧,希望对你们能有一点帮助。

本文首发于心安-XinAnzzZ 的个人博客,转载请注明出处~

二、线程池简介

1) 线程池是什么?

线程池就是指管理一组同构工作线程的资源池。每次应用程序需要创建一个线程来执行任务的时候不会直接创建线程,而是从线程池中取出线程,线程结束之后也不会直接销毁线程,而是放回线程留给其他任务使用。通过重用现有的线程而不是创建新线程,这样可以避免反复的创建和销毁线程,从而达到节省系统资源的目的。

2) 线程池的作用

我们通过一个对比来看一下线程池的作用。假如应用程序需要同时做三件事:读取磁盘文件、分析文件内容、写入数据库。

所以说,合理的使用线程池将会为我们带来以下好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

但是,要做到合理利用线程池,必须对其实现原理了如指掌。

3) 线程池是如何实现的

Java 中万物皆对象,线程池也是一个对象,在 Java 中使用java.util.concurrent.ThreadPoolExecutor这个类来实现线程池,这是线程池框架的最核心的类,也是后面我们分析线程池源码的核心对象,我们提前简单认识一下。既然是池,那就意味着它是一个容器,那么它是一个什么样的容器呢?阅读ThreadPoolExecutor类的源码可以发现它内部有一个类型为HashSet<Worker>workers字段,这个就是用来保存线程的容器。可以看见这个容器装的元素类型为Worker类型,这个是ThreadPoolExecutor的一个内部类,它实现了Runnable接口,也就是说它就是一个线程类。那么我们大体上就应该明白,每次需要新线程的时候就会创建一个Worker对象,然后加入到这个Set中。下面我说一下线程的工作流程再配以故事和图解:

4) 线程池是如何工作的

三、线程池源码分析

通过上面的讲解,相信读者已经能够明白线程池是什么、能做什么以及如何做的。那么下面就结合源码来剖析线程池的工作原理。以下所有源码均来自java.util.concurrent包下,这个包通常被简称为J.U.C。本文使用源码版本为Java8.

1) Execuor 框架

image

2) ThreadPoolExecutor源码分析

关于这些成员变量的含义,在ctl变量的注释中作者已经进行了详细的解释说明,如果你懂这些成员的意义并且你的英文能力不错的话,那么这个注释你读完一遍你就会发现,哇,作者写的真棒,但是如果你不懂或者你英文很烂,你就会发自肺腑的说一句,这什么破玩意。。

ok,不扯淡,笔者来解读一下作者的注释。首先,这个ctl它"打包"(原文是"packing")了用来表示线程池工作线程数量线程池运行状态的两个值。如何打包的呢?一个int型数据有 32 位,ctl的高 3 位表示线程池状态,低 29 位表示线程数量29 位大概可以表示 5 亿个线程。为什么是 3,而不是别的值呢?因为线程池状态有 5 种,分别为RUNNINGSHUTDOWNSTOPTIDYING以及TERMINATED,如果小于 3 位则不够表示 5 种状态,大于 3 位又浪费。到这里前面的成员变量的意义没什么问题了。为什么用一个 int 表示两个状态呢?作者的解释是更快更简单,我觉得不仅如此,还更装逼,嗯没错。熟悉读写锁ReadWriteLock的大神肯定清楚,读写锁也是用一个int 来分别表示读写锁状态。

再看一下下面三个方法,作者的注释翻译过来是:打包和拆包ctl,也就是我想获取runState咋获取,调用runStateOf,然后传入当前的ctl就可以了。再简单理解就是runStateworkerCount这俩玩意的gettersetter。下面是线程池不同状态对应的数值及其意义:

runState 对应的高三位的数值 原文 翻译
RUNNING 111 Accept new tasks and process queued tasks 接受新任务并且处理队列中的任务
SHUTDOWN 000 Don't accept new tasks, but process queued tasks 不接受新任务,但是处理队列中的任务
STOP 001 Don't accept new tasks, don't process queued tasks, and interrupt in-progress tasks 不接受新任务,不处理队列中的任务,并且会中断正在执行的任务
TIDYING 010 All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method 所有任务都已经结束,workerCount = 0,线程转换到 TIDYING 状态,将会执行 terminated()钩子函数
TERMINATED 011 terminated() has completed 钩子函数 terminated()执行完毕

所以,总结一下,上面的成员变量和三个辅助函数就是为了表示线程数量和线程池状态。下面看一下构造方法:

3) 核心方法源码分析

根据前面的流程分析,线程池核心就是提交任务,然后添加核心线程,添加到任务队列等等,一切的一切都始于提交任务,所以我们最先要分析的就是提交任务的方法execute(Runnable command),但是大概看一眼源码可以发现,这个方法本身只有一些逻辑判断,然后根据不同的逻辑去调用其他逻辑方法,而最多调用的是添加worker的方法addWorker(Runnable firstTask, boolean core)

所以想要理解线程池原理就要看懂execute方法,想看懂execute方法就要先看懂addWorker方法。下面我们就来分析一下addWorker方法,然后再分析execute方法。

4) Worker:工人是如何工作的。

前面已经简单的介绍了一下,WorkerRunnable的子类,也就是线程类。那么我们先看看它的结构。

5) 总结

在第二部分线程池简介的时候我们已经分析详细的描述了线程池的工作流程,但是那只是理论,这一节我们通过代码具体的了解到了线程池的运行原理。总结起来主要三个东西:

  1. 提交任务

    execute方法提交任务,然后根据池状态来判断是否接受任务,不接受采用拒绝策略;能够接受任务,判断是需要创建新的worker还是直接加入到任务队列;

  2. 添加 worker

    通过retry来不断地尝试,判断能否添加,不能返回false;能的话就尝试增加workerCount;然后创建worker,然后启动。

  3. 执行任务

    线程循环从任务队列取出任务来执行,直到队列为空。

四、总结

五、参考

上一篇下一篇

猜你喜欢

热点阅读