线程池讨论——20160424技术讨论

主讲人:浪子

参与者: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提供了大部分情况下都可用的线程池。如:固定数量的线程池、可调度任务执行时间的线程池、弹性的线程池几大类

线程池讨论——20160424技术讨论_第1张图片

平:http://ifeve.com/talk-concurrency/,http://ifeve.com/java-concurrency-thread-directory/

浪子:什么是 固定数量的线程池?答:指的是该类线程池所拥有的线程资源数量是有固定的,不会随着任务的增长而增多。这里面还有些细节后面讲源码时候会讲到,接下来几种线程池也是

浪子:至于线程池一些具体的使用demo这些我都不讲。只讲概念源码之类的

peng:在什么情况下使用能说说么?

乒乓狂魔:我们的重点是执行一个Runnable的逻辑过程。

浪子:使用场景很多,我也无法联想。知识都是一环扣一环的,并不是所线程池只是减少线程的创建、销毁。 比如你要考虑到你的CPU是几核的,应该创建多少线程,当前服务器上面是否有其他应用在大量的使用CPU,为什么使用?

那么你应该创建多少线程?线程池还有功能就是可以批量的多线程去同时处理多任务。这个在 JAVA并发实战里面也讲到线程该创建多少的问题,具体自己翻

浪子:@乒乓狂魔 ,这个的话,我就先提几个假设。
假设我们现在需要实现一个线程池,那我们的线程池需要拥有以下这些功能
1.可以根据自己需要设定该线程池维护多少个线程?
2.核心线程数,最大线程数?如果超过线程数的请求应该如何处理?
3.线程池的线程是否需要被回收?什么时候被回收?
4.一些边缘方法:如任务执行前处理,任务执行后处理,任务执行失败处理。线程池销毁等等

线程池里面最核心的一个接口就是ExecutorService,从下图也可以看到该接口的主要功能
如:销毁线程池,获取线程池状态,提交单个任务,提交批量任务(分两种)

线程池讨论——20160424技术讨论_第2张图片

我们使用线程池提交任务通常都是通过submit或者executor该图也能看到没有executor,因为ExecutorService继承了Executor接口,executor的方法在该父接口上面了

乒乓狂魔:上述有2个状态判断

线程池讨论——20160424技术讨论_第3张图片

浪子:恩,这些都是细节源码啊

浪子:这是我们上面的假设性问题,实际上我是从JUC线程池里面抽象出来的,那接下里看下线程池源码里面是如何实现这几个关系的

envy:我觉得从生产者消费者模式开始带入比较好

乒乓狂魔:这个应该不是太恰当

envy:线程池的原理应该算这个

乒乓狂魔:线程池要完成的核心逻辑就是你丢给它一个任务,它来帮你完成,原始接口就是这样定义的

线程池讨论——20160424技术讨论_第4张图片

浪子:线程池讨论——20160424技术讨论_第5张图片

    带入的因素很多

    1.线程开销

    2.提交任务,等待处理

    3.并发处理任务等

Barricelli:任务的提交和执行解耦

浪子:@Barricelli,从设计模式上讲,这也是很重要的一个,大家多多补充。

Lance:future 模式?

线程池讨论——20160424技术讨论_第6张图片

浪子:这图很具体反映了各接口和一些实现类的关系,主要的核心就是ExecutorService,线程资源的维护,任务调度都在这里

线程池讨论——20160424技术讨论_第7张图片

    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就能保证多线程的安全了。@乒乓狂魔 你可以看看源码。

浪子:我计算机基础不好,只想到这些语言描述他们了。。

线程池讨论——20160424技术讨论_第8张图片

Lance:对,没问题,我基础也不太好,但是看过听别人说过。

浪子:前面没化红线的几句是说该ctl维护两种数据状态。画红线的不知道能否回答狂魔刚才的问题。我英语不太好

线程池讨论——20160424技术讨论_第9张图片

    至于如何该类下的位运算,对于下面的源码还是很重要的。我这里给个程序你们自己观察

线程池讨论——20160424技术讨论_第10张图片

      根据这些位数自己推敲就能想明白了,虽然是个比较笨的办法,但是还是挺实用的。。我的读写锁也是这样分析明白的,即使没有位运算任何基础

浪子:接着看我们提交任务,任务是如何被处理的

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方法

线程池讨论——20160424技术讨论_第11张图片

浪子:submit你注意下他的形参。任务提交后进来这里处理的,但是这里面并没看到有执行任务的代码。是因为 具体任务执行都被封装到了Worker里面了,而线程池类的源码仅仅只是维护线程资源,和线程池相关功能。

    所以任务理应交到了线程资源 Worker

    线程池讨论——20160424技术讨论_第12张图片

    是被JUC封装过后的形参,所以灵活度较高。比如后面的可定时调节的任务就是通过这里的灵活度实现的,

    线程池讨论——20160424技术讨论_第13张图片

    这是我看线程池的类注释所记录的一些零散笔记

Lance:对,你说的对,注释就那么说的。

浪子:@lance 你说的是哪个最大线程数和队列的关系吧,那个我没细讲呢。只是看完,都没怎么梳理知识点,大家也可以找一些中文的JDK API看

Lance:嗯,貌似记得是。

浪子:接下来看Worker

    线程池讨论——20160424技术讨论_第14张图片

这个就是Worker类了,为什么它要继承AQS呢?

Lance:

    线程池讨论——20160424技术讨论_第15张图片

浪子:继承AQS的原因是因为他需要使用锁功能,该锁用于维护任务执行和该线程的关系。可以发现这里面仅仅只是个独占锁的功能,我们可以使用独占锁来做吗?不能,因为独占锁是可重入的,但是这里的实现是要求不可重入的。为什么不可重入 你们补充,我还没想到

    线程池讨论——20160424技术讨论_第16张图片

    线程池讨论——20160424技术讨论_第17张图片

    那么创建好了线程后,这个线程是如何执行任务的呢?

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:

    

浪子:对了,该锁除了重入功能不明白之外,还有就是有个功能判断当前线程是否存活,因为销毁线程池时候会判断线程是否存活。
    .shutdown  只是拒绝再接受新任务,但是还会处理队列里面的任务
    2.shutdownnow 直接中断所有任务
    shutdown  这里面就会需要是否判断线程是否存活,不存活的线程可以先关闭了。其他活的线程继续处理

    线程池讨论——20160424技术讨论_第18张图片

    谁来解读下这句话?

envy:为了提升任务的处理能力,加锁会导致任务处理比较慢

Lance:当我们调用线程池方法例如setCorepollsize方法的时候,我们不想让worker的任务重新获取lock,没理解,呜呜

浪子:我也是没理解。。是否重入和设置这个参数有什么关系吗

Lance:差不多就是envy的意思,就是避免重新获取锁,消耗性能

浪子:不是的,和性能没关系,非重入的锁 和 可重入锁哪个性能高?在同一个线程处理锁情况下

乒乓狂魔:首先想想w.lock()被哪些方法调用?会出现多个线程同时调用一个worker的lock方法吗?

浪子:我好像知道了,启动的时候

Lance:重入的性能好吧,说说。

浪子:任务启动和任务处理是分开的,如果一个任务(线程资源worker)被多次启动就有问题了

乒乓狂魔:但是你说的有可能吗?

浪子:也不对,@乒乓狂魔,求指教

乒乓狂魔:可以查下worker的lock方法被哪些方法调用

Barricelli:一个线程如果可以多次取任务执行,那统计corepoolsize这些就不对了

浪子:本来就可以多次取任务执行的@Barricelli

Barricelli

    线程池讨论——20160424技术讨论_第19张图片

浪子:怎么说?自己调用自己的任务还加锁?这个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

浪子:不是,和非重入也没关系,我再抛个新问题

    线程池讨论——20160424技术讨论_第20张图片

Lance:其他的线程池~我也不知道啊,注释是这样的意思,防止正在执行的task中断。

Barricelli

pool1 = Executors.newFixed();
pool2 = Executors.newxFixed();

浪子:这个和锁完全不着边啊

Barricelli:while循环呢,没任务了就不执行task嘛

浪子:我这个问题不用回答了,我搞错了,不是没任务,是线程已到达被回收状态

envy:其实你们要知道一点,work 是一个宝贵的资源

乒乓狂魔:目前只有一个地方被调用,就是Worker的主流程,Worker的主流程是一个线程在负责的,所以我是没看出来lock有什么用

    线程池讨论——20160424技术讨论_第21张图片

浪子: 我没什么讲的了,你们补充好了 
乒乓狂魔:FutureTask呢?还有刚才的getTask()的详细逻辑

浪子:我翻翻,我以前写的一个,FutreTask有什么问题大家提出来讨论好了,getTask挺有意思的,里面还管理了线程资源是否被回收问题

    线程池讨论——20160424技术讨论_第22张图片

    FutreTask 这我很早看了,都没准备呢,只能针对某个点讲,你提问题,我们就好讨论了,系统的话看文章就好,我看看getTask 就说这个,之前也没认真看这里

浪子:最后一个

    线程池讨论——20160424技术讨论_第23张图片

    这里面的超时运用的很巧妙,完全尽可能利用JDK相关工具代码来实现的,而不自己造轮子,又在自己代码里面写一遍超时算法。   直接运用了阻塞队列

Barricelli:这个问题还没清楚吧

    线程池讨论——20160424技术讨论_第24张图片

乒乓狂魔:setCorePoolSize目前跟Worker的lock没啥关系,你看下setCorePoolSize的代码

Barricelli:和重入有关系,和lock是没关系,setCorePoolSize里的interruptIdleWorkers方法,最终调用

    线程池讨论——20160424技术讨论_第25张图片

    注意这里的for(Worker w:workers)

乒乓狂魔:貌似是的,哈哈

Barricelli:lock的作用是防止其它pool的中断,前提是task在"executing",注释里用的是executing,那么应该对应的是task.run()的逻辑。

    线程池讨论——20160424技术讨论_第26张图片

乒乓狂魔:空闲线程需要中断,一旦发现线程没有占用lock则该线程就判定处于空闲状态

浪子:这个tryLocak的线程不是worker里的线程

    线程池讨论——20160424技术讨论_第27张图片

乒乓狂魔:是的,这些外部线程感觉就指的是文中说的other pool

Barricelli:是的

浪子:那怎么重入了呢

Barricelli:这里是不允许重入了

    线程池讨论——20160424技术讨论_第28张图片

浪子:外部线程本来就不存在重入概念啊,他都不是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 让我们看到一点转机 

你可能感兴趣的:(线程池讨论)