根据JDK深入详细学习理解JAVA线程池
笔者,之所以写这篇博客,是因为,很多博客或者书籍,在介绍线程池相关内容的时候,太理论化,直接上结论,并不能正确的表述出来所说的每一个结论,是为什么,怎么理解。所以在此记录一下自己的学习过程,大家共勉!
提前说明,本文文章内容有些深入,繁杂,显得自然就有些混乱,所以,请静下心来,慢慢理解!
首先,进行这样一个思考?
线程池,是什么?作用是什么?(为什么我们需要使用线程池?)
这个问题,相信很多地方都直接告诉你,线程池的好处,但是你有没有仔细想过,那些所谓的好处是怎么实现的?
话不多说,回到问题。
想一下这样一个问题,假如有一组客户端请求,同时访问服务器,服务器为了解决并发问题,就需要为每一个请求开启一个线程,保证客户端都能够及时的得到处理。那么,这个过程,产生一个这样的问题,每个请求的处理很简单,时间非常短,并且请求的基数很大,那么,我们这样做,cpu的处理器大部分时间都在干嘛?很容易有这么一个结论,CPU竟然一直在不停的创建和销毁线程。显然,这对于程序本身来说,是不合理的。
这里补充一点额外的知识,就是Java对于线程的创建和销毁的代价是远远大于普通类的。这一点,很容易理解,线程需要实现很多底层的操作系统级别的交互。他会需要很多资源去配合。
那么,上述并发问题,我们怎么解决呢?我们的主角,线程池登场了。
那什么是线程池呢?其实可以把线程池理解为一个并发框架,主要就是为了解决类似上述的问题。
我们来看看一些书籍,或者论文博客直接给出的线程池的好处的结论:(这里说明一点,大部分博客都没有给出理由,本文章后续会结合jdk源码,一点点理解这些所谓的好处是怎么实现的。如果你嫌麻烦,请自己跳转至下方红色标题处,直接看杰伦,哈哈,是不是很贴心!)
1.降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁的代价。
2.提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
3.提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优,和监控。
如果你本来就对线程池,有一些基础的理解,那么你仔细想想上述的好处,你真正理解是为什么吗?
其实,上述的几条好处就是,我们为什么要使用线程池。
如果,你之前根本不知道关于线程池的任何东西,看到这里是不是一脸懵?内心是这样的,这博客写的什么啊,既没有真正的说出来线程池是什么,也像别人一样直接照搬结论,也没有说出来为什么?真垃圾啊,,,
哈哈,不要慌,干货来了。因为,通过我个人的学习,我觉得线程池,是没有必要使用一些所谓的定义文字去抽象的描述它的,因为它本身就是为了解决某些并发的问题,所以,我们还是从底层的实现去理解。上述的这些描述,只是,让你对所谓的线程池有一个抽象笼统的认知。
线程池的相关实现原理,以及与其相关的知识剖析。(JDK1.8)
java中有一个底层实现的线程池类,ThreadPoolExecutor,我们就根据这个类来学习jdk中线程池的实现。
我们,先来看一下所谓的线程池这样的框架,其实现流程是怎么样的?(提醒你,如果你看不懂很正常,因为线程池的内容,本身就是相互关联的,而文章必须从某一点切入解释,所以如果你现在看不懂,整个文章看完,你一定会懂的!)
线程池,内部维护这很多成员变量,现在你需要知道这几个:核心数量,最大数量,阻塞队列。(不理解无所谓,你现在不需要理解,你只需要知道有这三个变量)
1.任务提交到线程池,首先判断,线程池内部已经创建的线程池的数量是否小于核心数量,如果小于,就创建一个新的线程来执行提交的任务。
2.如果1中,已经存在的线程数大于等于核心数量,则把提交的任务放进阻塞队列中,等待正在执行任务的线程执行完任务后,来执行阻塞队列中的任务。
3.在放进阻塞队列的时候,那自然也要判断阻塞队列是否已满,如果没有满,就放入进去,如果已经满了,这时候需要去判断最大数量,如果线程池中存在的线程数小于最大数量,则再创建新的线程。如果数量已经等于最大数量了,那么此时提交的任务,必须交由一种叫做饱和策略的逻辑去处理它。
使用线程池无非就是创建,使用(执行任务),销毁的过程。
线程池的创建
我们来看ThreadPoolExecutor的构造方法:
当程序中使用该类的时候,我们来看一下JDK底层的实现:
进入到源码,我们会发现,其构造函数有很多个:如下所示:
此时的你,如果仔细的看了这几个构造函数,你会发现,所有的构造函数其实最后都是执行了第四个的逻辑。也就是代码中的this(.....)方法,前三个构造函数,都在其内部显式的调用了第四个构造方法。
这四个构造方法,其实就是基于形式参数的四个方法重载,而最后都执行了第四个重载的构造方法!
那里面的第四个构造方法,我们发现他的形式参数是最多的,涉及到了七个。
1.corePoolSize:核心线程数量,这时候你是不是想到了执行流程中的这个参数,哈哈
2.maximumPoolSize:线程池最大容量。
3.keepAliveTime:线程活动保持时间,线程池的工作线程(所谓的工作线程,就是执行任务的线程)空闲后(所谓的空闲,就是把刚才其执行的任务执行完了,并且此时阻塞队列没有任务需要执行),保持存活的时间。这个参数很好理解,就是线程池内的线程在没有任务需要执行后,在存活多长时间才去销毁他,如果设置为0,那就是马上销毁。
4.TimeUnit:一个keepAliveTime的单位,自行查看jdk,他是一个枚举类型,内部是可选的时间单位。
5.BlockingQueue:阻塞队列,就是设置线程池所采用的阻塞队列。后续会介绍。
6.ThreadFactory:线程工厂,用于设置创建线程的工厂,无需多理解。
7.RejectedExecutionHandler:饱和策略,四种,后续会介绍。
了解了这七个参数,我们返回构造函数,理解构造函数的细节问题:
1.四个构造函数最后都是调用的最后一个七参数的构造函数。
2.四个构造函数的差异仅仅存在于外部传入的参数的数量,而正如jdk源码所看到的,前五个参数是一定要进行传入的,也就是无论外部显式使用的是哪一个构造方法,都必须指定前五个参数。而线程工厂和饱和策略没有要求外部必须指定。
3.既然6和7两个参数没有要求外部必须指定,那他的默认是什么呢?看上述源码,默认传入的是Executors.defaultThreadFactory(), defaultHandler这两个参数。工厂就不多说了,我们看看这个默认的handler
4.我们看看最终使用的第四个构造方法的注意事项:也就是上述代码中箭头所指的那两个if判断。其中第一个if要求了核心数量和最大容量之间的数量关系的限制,否则抛出异常。第二个if强制判断了阻塞队列,工厂,饱和策略不能为空。
现在我们来看看几种阻塞队列:JDK7提供了7个阻塞队列。
首先什么是阻塞队列?BlockingQueue是一个支持两个附加操作的队列。这两个附加操作支持阻塞的插入和移除方法。①支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。②支持阻塞的移除操作:意思是在队列为空时,获取元素的线程会等待队列变为非空。
注意,别矛盾,线程池只是用到了阻塞队列存储结构,并没有非要用到这两个阻塞方法,线程池进行具体的插入移除操作所采用的具体逻辑,是线程池自己的流程逻辑。可以返回文章上述中的线程池执行流程。
1.ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
2.LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
3.SynchronousQueue:一个不存储元素的阻塞队列
4.LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
5.PriorityBlockingQueur:一个支持优先级排序的无界阻塞队列
6.DelayQueue:一个使用优先级队列实现的误解阻塞队列
7.LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
由于阻塞队列,并不是本文章想要描述的内容,所以这里简单看一下即可!
1.ArrayBlockingQueue:直接看源码:基于数组的队列,需要传参指定大小,默认顺序为FIFO
2.LinkedBlockingQueue;基于链表的队列,大小参数可以指定,也可以不指定,不指定默认无穷,顺序FIFO
(FixedThreadPool()使用了这个阻塞队列。)
3.SynchronousQueue:一个不存储元素的阻塞队列。
(CachedThreadPool()使用了这个阻塞队列。)
好的,我们在来看看四种不同的饱和策略:
1.AbortPolicy;直接抛出异常。
2.CallerRunsPolicy:只用调用者所在线程来运行任务
3.DiscardOldestPolicy:丢弃阻塞队列里最近的一个任务,执行当前任务
4.DiscardPolicy:不处理,丢弃掉
好了,基于此,或许你还有很多疑问,但是至少对于线程池的基本变量有了初步的认知了。
向线程池提交任务:
向线程池提交任务,其实就是线程池如何去处理你所提交的任务,哈哈,终于到了线程池的底层本质了!
我先剧透告诉你,哈哈,提交任务的两个方法,execute()和submit()
我们先来回顾一些基础知识:创建线程的几种方法:
1.继承线程类Thread,重写run方法
2.实现Runnable接口,实现run方法
3.实现Callable接口,实现call方法。(注意理解该方法的区别)
虽然看似,三种方法,其实,真正意义上作为线程类的只有前两种方法,自行体会吧,每个人的理解都不同。我的理解是,自定义线程的本质是利用了Thread类的构造方法,而Thread的构造方法虽然有很多,但都是基于Runnable接口的,就连Thread类本身都是实现Runnable接口的。而任何Thread类的构造方法都没有涉及到Callable接口,并且callable只有在线程池的submit下才有其实现意义,所以可以简单理解为callable接口是线程池产生后衍生出来的一种东西。
好了,稍微理解一下上述辩述,我们来看一下执行任务的两个方法:
1.execute:只能执行Runnable接口的任务:
我们进入到源码去分析它,重点来了,这时候,你慢慢就会理解线程池的内部逻辑了:
好了,大概浏览,发现他其实和一个addWorker方法有直接联系:继续看:
addworker方法很长,下面四张截图应该是一起的!(其实你只需要看箭头,哈哈)
.
好了,是不是很蒙,我简单帮你梳理一下,execute()方法调用addWorker(),addWorker()方法中又出现了Worker类,Worker类是什么呢?先简单描述一下,下面会附上代码!Worker其实是线程池的本质,线程池,接收任务提交后,真正去执行的就是这个Worker类,也就是说Worker类是线程内部所维护的线程。是由这个Worker类的执行逻辑去执行任务的。先理解到此,后续有更深的理解。
来看一下Worker类的定义,以及构造方法:
创建Worker对象实例的时候,显然传入了一个Runnable接口!而Worker有成员变量firstTask(任务)和thread。而这个thread是干嘛的呢?其实很简单,就是实现了一步功能:由于Worker类是实现了runable接口,而实现runable接口的线程类在作为线程使用的时候,还得基于Thread类的构造方法,所以这一步,this.thread = getThreadFactory().newThread(this);这句语句把Worker又装饰成了Thread。
好了,简单理解了Worker后,我们此时再回到上面的addWorker介绍的最后一段代码,也就是着重标记(三个箭头指向)的那个地方,t.start();好了真正的boss要上场了,我们来看看strat()启动Woreker之后,所执行的run()方法。
好了,其中又设计到了runWorker方法,我们再进入到runWorker()方法
好了,到此,线程池底层的实现原理终于有了眉目了。哦,原来是在Worker线程中执行了任务的run()方法,哈哈。
哎,等等,我们是不是发现了一个重要的东西,如果你逻辑很敏锐,你发现,我们提交的Runnable接口的任务,在线程池底层,原来是直接执行的run()方法,而不是start(),哇哦,jdk源码关于线程池的设计真的是太精妙了!
这个调用run()方法的思路给了我们什么启示呢?线程池的核心!
还记得文章开头提到的线程池的好处吗?最重要的就是,线程池重复利用已创建的线程来降低线程创建和销毁所造成的消耗。无论当时的你是怎么理解这条好处的,现在的你应该,深入理解了吧!
原来,底层并不是让任务类直接作为线程执行,而是把任务封装到Worker线程中,开启Worker线程,而Woker线程中直接执行任务的run()方法,且Worker是线程池维护的线程,从而达到重复利用worker的目的。
这时候,如果你没理解什么意思,你就需要理解一个线程类的start()方法和run()方法的区别了。start方法是开启线程,并分配给线程除cpu执行外的所有所需资源,这个资源是很多的,所以说线程的代价是很大的。而直接执行run()方法,就简单多了,就像正常类执行正常的成员方法一样,这样就达到了降低资源消耗的目的。其实如果你只有几个线程的并发,线程池的作用,并不大,因为,他还是需要创建Worker线程,但是,如果你有数以万计的任务提交,那通过反复使用worker线程的方法,自然就体会到线程池这个思路的庞大意义了。说到这里,你也会感到,其实线程池提交任务,是把线程的创建销毁(Worker.start())和任务的执行(Runnable.run())分离开了,达到了线程的重复利用。
理解了上述问题,你也应该知道了线程池的第二条好处,就是响应,任务提交直接执行,这就不多说了,因为Worker已经维护在线程池中了,只要有空闲的Worker线程,那你提交任务的时候,就省去了创建线程的时间,直接让Worker调用提交任务的run()方法,多伟大的思路啊,佩服!哈哈。
至于,第三条好处,不多说,线程池作为一个线程容器的身份,自然就是去更好的组织管理线程了。
回到问题,关于任务执行,还有一个submit方法,我们来简单学习一下;
简单的介绍不多说,上述代码的注释中,也有说明,我们来看看submit方法的源码:
简单分析一下:submit有几个方法重载,重载的逻辑是传入参数的接口类型!
我们看到底层执行的execute()方法;那这个execute和ThreadPoolExecutor是一个方法吗?哈哈,是的!
首先,submit()方法,就不是ThreadPoolExecutor的。我们一点点分析:
原来,ThreadPoolExecutor继承了一个AbstractExecutorService类,但是此类是一个抽象类,不能实例化,通过jdk源码的查看,我们发现,ThreadPoolExecutor类中并没有submit方法,也就是作为子类,他没有重写submit,所以他调用的是继承自父类AbstratExecutService的submit。看到这里你就应该明白了,此时submit中的execute方法,他的所属类应该是抽象类引用的实际类ThreadPoolExecutor。这时候,你应该明白,ThreadPoolExecutor中的execute方法其实是对上层抽象方法的实现,哈哈,逻辑很敏锐。
那么我们,稍后再看这个execute().因为此时,你发现又出来个RunnableFuture类。
但是,细心的你,一定发现了其中每个方法的第三行代码,返回一个RunnableFuture,无论传入的参数是哪个接口,都是以这个方法进行了封装,我们继续看这个东西的源码:
(1)
哇哦,原来这个新东西继承了Runnable和Future接口。Future接口是什么呢?你现在只需要知道,submit方法的返回值是一个Future接口的具体实现类就行。
(2)
好了,上述submit方法中,涉及到了newTaskFor()方法,我们发现该方法又返回一个FutureTask类型的实例。
我们继续看FutureTask的实例。
(3)
这个类实现了RunnableFuture的接口,下面是其构造方法。
(4)
(5)
哇哦,这次的信息量有点大,不过没有关系,稍安勿躁。我们发现传入的是Callable接口的时候,并没有进行什么额外的操作,那么传入的是runnable接口的时候呢?看(5)中代码第二行,this.callable = Executors.callable(runnable, result);这是在干嘛呢?
直接看参数,传入Runnable,返回Callable,哦,是不是有印象,采用了一种设计模式,把Runnable接口封装成了Callable,目的就是为了统一处理这两个接口。哇哦,前辈的思路,真的很厉害!
总结上面这一段的思路,就是调用subimit方法,其实是调用的父类的submit方法,而该方法,会首先把传入的接口,包括runnable和callable接口封装成一个RunnableFuture,其实本质是FutureTask,而封装的方法就是newTaskFor,该方法又统一处理runnable和callable,因为该方法底层又把runnable封装成callable处理了。然后执行ThreadPoolExecutor中的execut方法,此时传入的参数是RunnableFuture,当然了继承Runnable接口。
此时,你应该立刻就想到,submit中本质上执行的execute方法,传入的是FutureTask,而其实现了Runnable接口,所以,submit底层的提交后,Worker线程中执行的应该是这个FutureTask的run()方法,来看一下:
果不其然,在其run方法中有调用了Callable的call方法。看上述箭头明白两件事情:
就是关于这个方法的
①在submit中调用newTaskFor方法,如果传入callable接口,那所调用的FutureTask的构造函数,其实就是把这个callable接口的实例关联进了FutureTask的一个Callable类型的成员变量。所以在run方法中直接得到所关联的Callable的实例。
②如果submit中调用newTaskFor,传入的是runnable接口,那在调用Futuretask的构造方法的时候,就需要额外的操作。
把Runnable必须包装成callable接口类型,因为FutureTask的run方法就只是针对callable的,没有任何关于runnable接口类型的处理。所以就有了:
如果你看到这里,感觉有点乱,那我把这些类的继承关系稍微梳理一下:
1.关于ThreadPoolExecutor:
这里关于ThreadPoolExecutor多说一句,就是基于Executor框架的工具类Executors,可以创建三种类型的ThreadPoolExecutor。
①FixedThreadPool,可重用固定线程数的线程池。阻塞队列是:LinkedBlockingQueue
②SingleThreadExecutor,使用单个Worker线程的线程池,阻塞队列也是:LinkedBlockingQueue
③CachedThreadPool,根据需要创建新线程的线程池,阻塞队列是,SynchronousQueue
2.关于Future的层次关系:
这里只是做一个简单的梳理,其实线程池的框架Executor还涉及很多类和接口,自行学习,不在本文章的讨论中。
好了,我们看一下,调用submit的具体使用代码:
个人的简单练习代码,在文章最后有上传,如果你是初学者,可以复制到自己的编译器中学习一下,很简单,只是作为一个学习参照使用,没有其他作用,作者在此也只是利用练习代码叙述jdk的源码原理。
好了,看到这里,相信你现在已经不是线程池方面的小白了!
因为,我们到了最后一步了,很简单,关闭线程池!
关闭线程池
两个方法,如上面代码中箭头所指内容!
原理:
遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的线程可能永远无法终止,但是他们既然作为两个方法,必然有其特定的区别:
1.shutDownNow方法,首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
2.shutDown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
所以,你会有这样的结论:一般我们采用shutdown方法,因为他不会打断正在执行任务的线程。如果任务不一定要执行完,我们强制中断,则可以调用shutDownNow方法。
看到这里,相信,再被问到关于线程池的相关问题的时候,你的内心是这样的:
都给我让开,LZ要装逼了。
最后附上文章中所用的简单练习代码:
import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolTest { public static void main(String args[]) { //这里只是展示几种阻塞队列,jdk7提供了7种 BlockingQueue<Runnable> aBQueue = new ArrayBlockingQueue<Runnable>(10); //基于数组结构的有界队列 BlockingQueue<Runnable> lBQueue = new LinkedBlockingQueue<Runnable>(); //基于链表结构的队列 BlockingQueue<Runnable> sBQueue = new SynchronousQueue<Runnable>(); //一个不存储元素的阻塞队列 BlockingQueue<Runnable> pBQueue = new PriorityBlockingQueue<Runnable>(); //具有优先级的无限阻塞队列 //线程池的基本实现类,最大构造函数有7个底层参数,其他所有构造函数内部都是调用最大构造函数 ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 6, 0, TimeUnit.DAYS, aBQueue); tpe.execute(new RunnableTest()); //execute只能执行Runnable,无返回值 tpe.execute(new ThreadTest("线程1")); //没有问题,Thread本身就实现了Runnable接口 //start方法的本质: //Thread内部维护着一个Runnable类型的成员变量target //然后Thread本身又实现了Runnable的接口,内部实现run()方法 //该方法,就是调用target的run()方法, //target在构造函数时,就传入了一个Runnable接口的实现类 (new ThreadTest("线程1")).start(); //submit可以执行Runnable,此时方法也是有返回值的,也是返回Future接口 //底层实现其实submit把runnable接口封装成了callable tpe.submit(new RunnableTest()); //submit还可以执行Callable,方法有返回值,返回Future Future<String> future = tpe.submit(new CallableTest()); try { //Future只是个接口,其真正实现类都是FutureTask //真正的关系是,RunnableFuture接口继承了Future和Runnable //FutureTask实现了RunnableFuture接口 String aa = future.get(); System.out.println(aa); } catch (Exception e) { e.printStackTrace(); } System.out.println("程序正常运行无异常!"); tpe.shutdown(); //pe.shutdownNow(); } } //封装程序中用到的几个线程 //继承线程的方法,实现线程 class ThreadTest extends Thread { public ThreadTest(String name) { super(name); } //重写run方法 public void run() { System.out.println(Thread.currentThread().getName()); } } //实现Runnable接口的线程类,无返回值 class RunnableTest implements Runnable { public void run() { for(int i = 0; i < 10; i++) { System.out.println (Thread.currentThread() + "程序子线程循环的次数:" + i); } } } //实现Callable接口的类,有返回值 //注意这种方法,只能配合线程池使用,并不能够单独的启动线程 //因为Thread的所有构造方法,都没有能够传入Callable接口的 //除非手动的再实现Runnable接口,或者继承Thread类 class CallableTest implements Callable<String> { public String call() { return new String(Thread.currentThread().getName()); } }