再来说一说线程池
这段时间一直在忙着自己的事情,搞的非常尴尬,一直没能静下心来好好想想。不过还好,最近对于线程池的东西还是一直处于进步阶段。
我还是习惯直接动嘴说说,以后万一想开了,也会贴很多源码上来,但是现在主要还是想打字来解释,毕竟源码很多时候来的更清晰,但是我这样写的目的也是等于加强自己的记忆,所以先不用源码来增加篇幅了。
还是先new一个线程池开始吧。new ThreadPoolExecutor(0,0,0,null,new SynchronousQueue<Runnable>());0和null是我上一篇已经详细说的东西了,现在实在是懒得给他多一点赋值的心情了。对,没错,我是打算说一下最后这个队列的。
这个队列,目前最常见的有三种(ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue),在使用executors创建的线程池中,你会发现没有第一种,具体原因不得而知,可能是:一、限制的太死?因为array对列是固定缓存池大小的(因为没人会问这个队列是干嘛的了吧,好吧,我说一句,这个是用来缓存任务的,只有当任务填满缓存了,才会用到maxinumpoolsize,上一篇。。。)。这种固定死的方式会导致很多意想不到的拒绝(线程池对任务的拒绝措施,一般都是默认的----抛异常拒绝)。而且我感觉推荐使用executors也是有一种不在拒绝的味道(个人主观臆断,没什么依据,主要是从创建的几种线程池的内容中推断的,毕竟我只是一个渣渣,后面我会说一点点推理的依据)。二、设计不好(求不打脸,我这个瞎说的),看过源码的童鞋应该发现了,array队列的里面只用一把锁final修饰的。这样这个队列在添加和移除的时候都是同一把锁,也就是不能出现添加和移除的并发操作,我大致百度了一下,没有找到合理的解释。这样可能会在线程池频繁添加和移除的时候出现性能问题。当然这个是我瞎说的,不注意作为依据。
那我们再来谈谈剩下的两个queue吧。linkedBlockingQueue,一看名字就知道,这个是链表的队列嘛,对,就是这样的,那有什么说的呢?首先说一下锁的问题,这个队列就很理智(233)的使用了两个final的lock(take和put),对嘛,进出分开嘛,当年我在化工厂里面看设计图的时候,就看到人家设计师说,人流物流进进出出严格分开滴,没错,程序就是要反应现实的,不然写程序不就找不到使用的价值了(继续瞎说)。然后再来一点不瞎说的。既然是链表,我们存取是不是可以使用双向链表尾放头取呢?可以,没错,人家就是这么玩的,是不是一点都不意外,你能想到的这么简单的东西,人家大牛早就实现了。那么,这样是不是就有一个很熟悉的生产者消费者模式出现了,哇塞,如果你能想到这里,那么和我这个渣渣同一水准了呢(2333),反正写东西的时候,瞎想一下,万一和大神想到一起去了呢。对,这里就是这样的。通过前面的两把锁(显示锁aqs实现的ReentrantLock,我打算下一次写一下这个),获取condition(用来休息的),然后每次存/取的时候,检查一下容量,如果满/亏,那么就休息一下。至于为什么是循环,那是因为这里有并发,每次醒来的时候,先循环一下,能不能拿到,拿不到就继续休息。如果拿到了,就去干活吧。take和put最后一行代码是叫醒对应的线程,这个是因为这个线程已经拿走了一个,所以让等着的put填进来。(这里我专门提一下的原因是我第一次就看错了,搞了半天才发现是自己atomicInteger的方法getAndDecrement搞错了---拿到旧值(我理解成了拿到增加之后的值了)),所以这里我专门提示一下。然后这个队列就没有太多可以说的了,而且实现也很简单,看一遍源码就知道了。
剩下一个是SynchronousQueue,这个没有加blocking,是不是以为就不是阻塞的呢?这个队列很特殊,它是一个转移队列,自己不保存东西,也就是容量是0,。那他怎么玩的呢?首先通过内部transfer对象来实现(queue公平,stack非公平)队列。说好的不保存东西的呢,是的,任务过来了,它是不会保存的。那么他保存的是什么呢,没错,是空闲的线程。这个队列通过put的东西会一直阻塞自己,只有当下一个线程过来take之后才会继续自己的逻辑。所以当新任务来的时候,这个队列直接创建新的线程去执行,自己不缓存任务。而当空闲线程回来的时候,那就可以进队了,因为put就是await当前线程的,这个是通过lockResource也就是底层native方法的wait,因为这个队列没有使用aqs,直接是使用的cas算法,代码那叫一个复杂呀。不过慢慢也能看懂,就是node的进出队列。算是一个优化锁的实现,先自旋一会然后等待(根据cpu数定次数,如果单cpu,就不要自旋了,直接wait吧,其他的就是需要的),这个设计就非常合理,因为单cpu如果大量的自旋,会明显拖慢其他的线程。至于这个队列的公平模式和非公平模式,我就不细说了,感觉和锁的机制差不多,只是他使用了两种数据结构罢了。
说了这么多废话,现在再说一下java的推荐的几种选择,单线程和固定容量的线程都是选择的是LinkedBlockingQueue,没有大小上限,默认是integer.MAX_VALUE(当然可以设置大小的),前面我说的不建议使用拒绝措施就是这里看到的,因为如果核心池和最大池固定了,就不再固定缓存的数量了,这样在正常的情况就不会有什么拒绝的机会了。至于cached的线程池,那还用说,虽然没有核心线程,但是我们最大线程多呀,也是integer.MAX_VALUE(其实线程池没有这么大的容量,因为线程池容量和状态是一个integer,32位,高位3个位置保存status,低位29个位置保存容量,这种设计,省了一个atmicinteger的对象,但是对于我这种渣渣,看起源码来,苦的一批),所以,这个缓存池也没有机会拒绝,对了,差点忘记说了,这个队列使用的就是SynchronousQueue,每次过来任务也不缓存了,直接创建新线程就可以了,反正我们线程多,用来了过期时间到了也自动销毁了。不过这种方法其实不是很好,因为对于任务时间长的任务,会出现大量的运行和就绪的线程,导致其他的线程运行速度受限,所以最好使用这个线程池的时候,执行的都是小任务。
好了,线程池就到这里吧,其实很多线程池的东西可以写,但是写起来真的没完没了了,比如每一个线程的创建都是一个work,work不单单实现了runnable接口,还继承了aqs,没错,每一个任务本身就是一把锁,而且是排他锁。好了,大致就这么多了,下一篇,我们聊聊锁吧。