主讲人:浪子
参与者:Lance,乒乓狂魔,envy,Barricelli,请叫我阿勇
记录者:peng
大家准备好,要开始了,有请浪子出场!
浪子:为什么要使用线程池?假设我们要实现一个线程池要怎么实现?
一、 Executors 1.各种常用的线程池
二、ThreadPoolExector
1.线程池里成员变量的意义?
2.线程池所支持的功能
3.线程池关键的代码
1)executor后,线程池如何调度任务?
2)线程池的位运算
3)线程池的一些扩展功能
Lance:线程池几个重要的接口,类图,其中的队列是哪种队列
浪子:这个问题的话就从假设我们实现一个线程池如何实现开始吧、我没怎么准备的,只是看了代码。有错的地方大家指出来
Lance:没问题啊,你juc比较强
Lance:
1、线程池的基本使用
1.1为什么要使用线程池的原因 简单的线程池的实现
1.2 JDK为我们提供了哪些内置线程池
1.3 线程池的使用
1.3.1 线程池的种类:newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool
1.3.2 不同线程池的共同性 线程池构造函数详解
1.4 线程池使用的小例子
1.4.1 简单线程池
1.4.2 ScheduledThreadPool
2.扩展和增强线程池
2.1 回调接口:beforeExecute,afterExecute,terminated
2.2 拒绝策略
2.3 自定义ThreadFactory
3.线程池及其核心代码
浪子:为什么我们需要使用线程池?
答:我们平常也经常会接触到连接池这个概念,连接池通常存放的是一些可复用的资源,避免每一次的使用都需要重复创建这些资源,我们可以把这些资源都保存在一个池子里,当需要用时候从池子里面取,用完了放回去。如果同时很多请求过来请求资源时候,由于资源的数量有限,我们可以把这些请求又放在一个队列里面,依次等待线程池的复用。如:数据库连接池等
浪子:维护一个线程是需要很大开销的,包括线程的创建,销毁等。而且JVM通常所能创建的线程也是有限的,和JVM的所管理的内存区域有很大关系,比如通过降低JVM的堆内存来提高线程堆栈的内存来提升可创建线程的数量。这些具体的得去了解JVM了。。
浪子:通常我们使用线程池都是通过一个线程池的工厂方法来使用的,这个类是 Excutors
Lance:工厂类Excutors
浪子:Excutors提供了大部分情况下都可用的线程池。如:固定数量的线程池、可调度任务执行时间的线程池、弹性的线程池几大类
平:http://ifeve.com/talk-concurrency/,http://ifeve.com/java-concurrency-thread-directory/
浪子:什么是 固定数量的线程池?答:指的是该类线程池所拥有的线程资源数量是有固定的,不会随着任务的增长而增多。这里面还有些细节后面讲源码时候会讲到,接下来几种线程池也是
浪子:至于线程池一些具体的使用demo这些我都不讲。只讲概念源码之类的
peng:在什么情况下使用能说说么?
乒乓狂魔:我们的重点是执行一个Runnable的逻辑过程。
浪子:使用场景很多,我也无法联想。知识都是一环扣一环的,并不是所线程池只是减少线程的创建、销毁。 比如你要考虑到你的CPU是几核的,应该创建多少线程,当前服务器上面是否有其他应用在大量的使用CPU,为什么使用?
那么你应该创建多少线程?线程池还有功能就是可以批量的多线程去同时处理多任务。这个在 JAVA并发实战里面也讲到线程该创建多少的问题,具体自己翻。
浪子:@乒乓狂魔 ,这个的话,我就先提几个假设。
假设我们现在需要实现一个线程池,那我们的线程池需要拥有以下这些功能
1.可以根据自己需要设定该线程池维护多少个线程?
2.核心线程数,最大线程数?如果超过线程数的请求应该如何处理?
3.线程池的线程是否需要被回收?什么时候被回收?
4.一些边缘方法:如任务执行前处理,任务执行后处理,任务执行失败处理。线程池销毁等等
线程池里面最核心的一个接口就是ExecutorService,从下图也可以看到该接口的主要功能
如:销毁线程池,获取线程池状态,提交单个任务,提交批量任务(分两种)
我们使用线程池提交任务通常都是通过submit或者executor,该图也能看到没有executor,因为ExecutorService继承了Executor接口,executor的方法在该父接口上面了
乒乓狂魔:上述有2个状态判断
浪子:恩,这些都是细节源码啊
浪子:这是我们上面的假设性问题,实际上我是从JUC线程池里面抽象出来的,那接下里看下线程池源码里面是如何实现这几个关系的
envy:我觉得从生产者消费者模式开始带入比较好
乒乓狂魔:这个应该不是太恰当
envy:线程池的原理应该算这个
乒乓狂魔:线程池要完成的核心逻辑就是你丢给它一个任务,它来帮你完成,原始接口就是这样定义的
带入的因素很多
1.线程开销
2.提交任务,等待处理
3.并发处理任务等
Barricelli:任务的提交和执行解耦
浪子:@Barricelli,从设计模式上讲,这也是很重要的一个,大家多多补充。
Lance:future 模式?
浪子:这图很具体反映了各接口和一些实现类的关系,主要的核心就是ExecutorService,线程资源的维护,任务调度都在这里
ExecutorService JDK1.6和 1.7差别很大,1.6的很容易懂,我昨天看错了,看的是1.6..今天看1.7发现多出好多东西,我接下来讲的是1.7的
Lance:好的,讲ctl ?这个有点难度,继续。
请叫我阿勇:http://blog.csdn.net/hsuxu/article/details/8985931
浪子:看过读写锁源码都知道,由于读写锁使用了AQS的state,该state只能代表一个状态。但是读写锁要控制读锁 和写锁的状态怎么办?它通过取该state的高低16位分别设为读锁和写锁的状态来解决该问题
ThreadPoolExecutor 情况也和读写锁差不多,它通过一个ctl, int型的数据类型来控制该线程池的状态和 该线程池当前所拥有的线程资源数量
乒乓狂魔:其实有必要必须这么弄吗?一个数据存储2个内容?
Lance:有必要,因为不这么用的话,那么你在一个类中要分成2个属性,那么2个属性在高并发下还需要lock等操作,这样做的话直接用位偏移之后cas就能保证多线程的安全了。@乒乓狂魔 你可以看看源码。
浪子:我计算机基础不好,只想到这些语言描述他们了。。
Lance:对,没问题,我基础也不太好,但是看过听别人说过。
浪子:前面没化红线的几句是说该ctl维护两种数据状态。画红线的不知道能否回答狂魔刚才的问题。我英语不太好
至于如何该类下的位运算,对于下面的源码还是很重要的。我这里给个程序你们自己观察
根据这些位数自己推敲就能想明白了,虽然是个比较笨的办法,但是还是挺实用的。。我的读写锁也是这样分析明白的,即使没有位运算任何基础
浪子:接着看我们提交任务,任务是如何被处理的
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }这个executor方法,我们通常就是使用它来提交任务。这段代码我只删减了一些注释
Lance:这里和用blockingqueue有关系吧?bq队列为空或者是满的时候线程都阻塞的!
请叫我阿勇:一般调用submit()方法都是将任务封装成RunnableFutute,然后执行execute方法
浪子:submit你注意下他的形参。任务提交后进来这里处理的,但是这里面并没看到有执行任务的代码。是因为 具体任务执行都被封装到了Worker里面了,而线程池类的源码仅仅只是维护线程资源,和线程池相关功能。
所以任务理应交到了线程资源 Worker
是被JUC封装过后的形参,所以灵活度较高。比如后面的可定时调节的任务就是通过这里的灵活度实现的,
这是我看线程池的类注释所记录的一些零散笔记
Lance:对,你说的对,注释就那么说的。
浪子:@lance 你说的是哪个最大线程数和队列的关系吧,那个我没细讲呢。只是看完,都没怎么梳理知识点,大家也可以找一些中文的JDK API看
Lance:嗯,貌似记得是。
浪子:接下来看Worker
这个就是Worker类了,为什么它要继承AQS呢?
Lance:
浪子:继承AQS的原因是因为他需要使用锁功能,该锁用于维护任务执行和该线程的关系。可以发现这里面仅仅只是个独占锁的功能,我们可以使用独占锁来做吗?不能,因为独占锁是可重入的,但是这里的实现是要求不可重入的。为什么不可重入 你们补充,我还没想到
那么创建好了线程后,这个线程是如何执行任务的呢?
envy:不可重入是为了一个work一次只处理一个任务
浪子:注释上是这么说的,为什么它会一次执行多个任务?什么情况会?
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt 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); } }这段是最核心的任务执行代码了,Worker维护了线程Thread和任务Runnable的关系和队列任务的关系
从这里面可以看到前面所说的,这是一个循环,一次执行执行一个任务(同步锁),如果处理完了当前的任务则从队列里面继承取任务出来处理,直到没任务处理那么该Worker就属于空闲资源,等待是否被回收了
乒乓狂魔:Worker本身就是只关联到一个线程,为什么还要用到锁?
Lance:@乒乓狂魔 看我截图,说是为了获取和释放锁。
浪子:@envy,说的一次只能执行一个任务,注释上面也是这么说的。但是我也没想明白,明明就是依次从队列里面取任务消费,有顺序性的,为什么该线程需要锁呢,大家可以先针对这个问题讨论下呗
Barricelli:
浪子:对了,该锁除了重入功能不明白之外,还有就是有个功能判断当前线程是否存活,因为销毁线程池时候会判断线程是否存活。
1 .shutdown 只是拒绝再接受新任务,但是还会处理队列里面的任务
2.shutdownnow 直接中断所有任务
shutdown 这里面就会需要是否判断线程是否存活,不存活的线程可以先关闭了。其他活的线程继续处理
谁来解读下这句话?
envy:为了提升任务的处理能力,加锁会导致任务处理比较慢
Lance:当我们调用线程池方法例如setCorepollsize方法的时候,我们不想让worker的任务重新获取lock,没理解,呜呜
浪子:我也是没理解。。是否重入和设置这个参数有什么关系吗
Lance:差不多就是envy的意思,就是避免重新获取锁,消耗性能
浪子:不是的,和性能没关系,非重入的锁 和 可重入锁哪个性能高?在同一个线程处理锁情况下
乒乓狂魔:首先想想w.lock()被哪些方法调用?会出现多个线程同时调用一个worker的lock方法吗?
浪子:我好像知道了,启动的时候
Lance:重入的性能好吧,说说。
浪子:任务启动和任务处理是分开的,如果一个任务(线程资源worker)被多次启动就有问题了
乒乓狂魔:但是你说的有可能吗?
浪子:也不对,@乒乓狂魔,求指教
乒乓狂魔:可以查下worker的lock方法被哪些方法调用
Barricelli:一个线程如果可以多次取任务执行,那统计corepoolsize这些就不对了
浪子:本来就可以多次取任务执行的@Barricelli
Barricelli:
浪子:怎么说?自己调用自己的任务还加锁?这个run只能worker里面的线程调用,外面的都无法接管啊,所以我也没明白
Lance:是不是这里啊。。我想应该是这里
* 2. Before running any task, the lock is acquired to prevent * other pool interrupts while the task is executing, and * clearInterruptsForTaskRun called to ensure that unless pool is * stopping, this thread does not have its interrupt set乒乓狂魔: 什么叫other pool
浪子:不是,和非重入也没关系,我再抛个新问题
Lance:其他的线程池~我也不知道啊,注释是这样的意思,防止正在执行的task中断。
Barricelli:
pool1 = Executors.newFixed(); pool2 = Executors.newxFixed();
浪子:这个和锁完全不着边啊
Barricelli:while循环呢,没任务了就不执行task嘛
浪子:我这个问题不用回答了,我搞错了,不是没任务,是线程已到达被回收状态
envy:其实你们要知道一点,work 是一个宝贵的资源
乒乓狂魔:目前只有一个地方被调用,就是Worker的主流程,Worker的主流程是一个线程在负责的,所以我是没看出来lock有什么用
浪子: 我没什么讲的了,你们补充好了
乒乓狂魔:FutureTask呢?还有刚才的getTask()的详细逻辑
浪子:我翻翻,我以前写的一个,FutreTask有什么问题大家提出来讨论好了,getTask挺有意思的,里面还管理了线程资源是否被回收问题
FutreTask 这我很早看了,都没准备呢,只能针对某个点讲,你提问题,我们就好讨论了,系统的话看文章就好,我看看getTask 就说这个,之前也没认真看这里
浪子:最后一个
这里面的超时运用的很巧妙,完全尽可能利用JDK相关工具代码来实现的,而不自己造轮子,又在自己代码里面写一遍超时算法。 直接运用了阻塞队列
Barricelli:这个问题还没清楚吧
乒乓狂魔:setCorePoolSize目前跟Worker的lock没啥关系,你看下setCorePoolSize的代码
Barricelli:和重入有关系,和lock是没关系,setCorePoolSize里的interruptIdleWorkers方法,最终调用
注意这里的for(Worker w:workers)
乒乓狂魔:貌似是的,哈哈
Barricelli:lock的作用是防止其它pool的中断,前提是task在"executing",注释里用的是executing,那么应该对应的是task.run()的逻辑。
乒乓狂魔:空闲线程需要中断,一旦发现线程没有占用lock则该线程就判定处于空闲状态
浪子:这个tryLocak的线程不是worker里的线程
乒乓狂魔:是的,这些外部线程感觉就指的是文中说的other pool
Barricelli:是的
浪子:那怎么重入了呢
Barricelli:这里是不允许重入了
浪子:外部线程本来就不存在重入概念啊,他都不是worker里面的线程
Barricelli:other pool的那个问题不是和重入有关,是和lock有关。
乒乓狂魔:它是不允许外部线程多次调用tryLock方法,而不是Worker本身内部线程的重入,是不允许外部线程的重入
浪子:嗯,似乎是,我明天看看外部哪里可能单个线程重入
Barricelli:我晕了,外部线程有重入的概念吗。独占模式就没有外部线程进入啊
浪子:@Barricelli,你误解了,外部单个线程重入,只有这样才可能有重入问题,之前我一直局限在内部重入里
Lance:http://mp.weixin.qq.com/s?__biz=MjM5NzMyMjAwMA==&mid=2651477075&idx=1&sn=ad1750b33663fe57465c481f2ff92b44&scene=0#wechat_redirect
乒乓狂魔:人多力量大,还是 @Barricelli 让我们看到一点转机