Java线程池的工作原理

                                                                                  线程池源码解读

随着cpu核数越来越多,不可避免的利用多线程技术以充分利用其计算能力。所以,多线程技术是服务端开发人员必须掌握的技术。

线程的创建和销毁,都涉及到系统调用,比较消耗系统资源,所以就引入了线程池技术,避免频繁的线程创建和销毁。

在Java用有一个Executors工具类,可以为我们创建一个线程池,其本质就是new了一个ThreadPoolExecutor对象。线程池几乎也是面试必考问题。本节结合源代码,说说ThreadExecutor的工作原理

一、线程池创建

先看一下ThreadPoolExecutor参数最全的构造方法:

①corePoolSize:线程池的核心线程数,说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。

②maximumPoolSize:最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。

③keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。

⑤unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。

⑥workQueue:一个阻塞队列,提交的任务将会被放到这个队列里。

⑦threadFactory:线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。

⑧handler:拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。

以上就是创建线程池时用到的参数,面试中经常会有面试官问道这个问题。

二、线程池执行流程

这里用一个图来说明线程池的执行流程

任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。

这里以jdk1.8.0_111的源代码为例,看一下具体实现。

1、先看一下线程池的executor方法

①:判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务

②:如果不小于corePoolSize,则将任务添加到workQueue队列。

③:如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。

2、再看下addWorker的方法实现

这块代码是在创建非核心线程时,即core等于false。判断当前线程数是否大于等于maximumPoolSize,如果大于等于则返回false,即上边说到的③中创建线程失败的情况。

addWorker方法的下半部分:

①创建Worker对象,同时也会实例化一个Thread对象。

②启动启动这个线程

3、再到Worker里看看其实现

可以看到在创建Worker时会调用threadFactory来创建一个线程。上边的②中启动一个线程就会触发Worker的run方法被线程调用。

4、接下来咱们看看runWorker方法的逻辑

线程调用runWoker,会while循环调用getTask方法从workerQueue里读取任务,然后执行任务。只要getTask方法不返回null,此线程就不会退出。

5、最后在看看getTask方法实现

①咱们先不管allowCoreThreadTimeOut,这个变量默认值是false。wc>corePoolSize则是判断当前线程数是否大于corePoolSize。

②如果当前线程数大于corePoolSize,则会调用workQueue的poll方法获取任务,超时时间是keepAliveTime。如果超过keepAliveTime时长,poll返回了null,上边提到的while循序就会退出,线程也就执行完了。

如果当前线程数小于corePoolSize,则会调用workQueue的take方法阻塞在当前。

                                                                        线程池是什么时候创建的

带着几个问题进入源码分析:

1. 线程池是什么时候创建线程的?

2. 任务runnable task是先放到core到maxThread之间的线程,还是先放到队列?

3. 队列中的任务是什么时候取出来的?

4. 什么时候会触发reject策略?

5. core到maxThread之间的线程什么时候会die?

6. task抛出异常,线程池中这个work thread还能运行其他任务吗?

先写一段基础代码,进入分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

public static void main(String[] args) {

        ExecutorService executorService = new ThreadPoolExecutor(250, TimeUnit.DAYS,

                new ArrayBlockingQueue<>(1), new ThreadFactory() {

            @Override

            public Thread newThread(Runnable r) {

                Thread thread = new Thread(r);

//                thread.setDaemon(true);

                return thread;

            }

        });

        // 对象创建后,线程实际还没开始创建

        // 执行execute时,检查当前池中线程数大小是否小于core number, 如果是,则创建新线程

        executorService.execute(() -> {

            System.out.println("任务1@" + Thread.currentThread().getName());

            sleepTime();

            System.out.println(1);

        });

        //检查当前池中线程数大小是否小于core number, 如果是,则创建新线程

        executorService.execute(() -> {

            System.out.println("任务2@" + Thread.currentThread().getName());

            sleepTime();

            System.out.println(2);

        });

        // 检查当前池中线程数大小是否小于core number, 如果不是,则偿试放入队列

        // 这个任务是加到队列去的, 注意队列大小只有1,

        // TODO 队列中的任务是什么时候取出来的?   任务1或者2结束后所占用的线程 会运行队列中的任务,这个任务是在最后才运行,比4运行的还晚

        executorService.execute(() -> {

            System.out.println("任务3@" + Thread.currentThread().getName());

            sleepTime();

            System.out.println(3);

        });

        // 检查当前池中线程数大小是否小于core number, 如果不是,则偿试放入队列,放入队列也失败,则增加新的worker线程

        // 这个任务是加到core以外的新线程去的

        executorService.execute(() -> {

            System.out.println("任务4@" + Thread.currentThread().getName());

            sleepTime();

            System.out.println(4);

        });

    }

  

注意第3行,创建一个核心池2, 最大池5, 队列为1的线程池

至少在new ThreadPoolExecutor()时,Thread对象并没有初始化. 这里仅仅指定了几个初始参数

执行第一个execute时,进入调试jdk源码

 代码块1

第一个if, 判断如果当前线程数小于corePoolSize, 则创建新的核心worker对象(Worker中指向Thread对象,保持引用,保证不会被GC回收)

     我们的示例代码中,第1和第2个任务都是这样创建出线程的

第二个if,   判断如果当前线程数大于corePoolSize, 并偿试放入队列 workQueue.offer(command) , 放入成功后等待线程池调度【见后面的getTask()】

     示例代码中,第3个任务是这样等待调度的,最后才执行

第三个if,  偿试放入队列 workQueue.offer(command) 失败, 增加一个非core的线程

     示例代码中,第4个任务是这样开始的

然后再看addWorker()的过程

 

new Worker构造和线程启动

线程启动后,又做了哪些工作:

异常分析

没抛异常时,此线程会一直在while(task !=null || (task = getTask())!=null)中跑

那么有异常时,再看一下processWorkerExit

 

可以 看出,有异常时 旧的worker会被删除(GC回收),再创建新的Worker, 即有异常时 旧worker不可能再执行新的任务

总结:

Q. 线程池是什么时候创建线程的?

A.任务提交的时候

Q.任务runnable task是先放到core到maxThread之间的线程,还是先放到队列?

A.先放队列!!!

Q. 队列中的任务是什么时候取出来的?

A. worker中 runWorker() 一个任务完成后,会取下一个任务

Q. 什么时候会触发reject策略?

A.队列满并且maxthread也满了, 还有新任务,默认策略是reject

Q. core到maxThread之间的线程什么时候会die?

A.  没有任务时,或者抛异常时。   

   core线程也会die的,core到maxThread之间的线程有可能会晋升到core线程区间,

   core max只是个计数,线程并不是创建后就固定在一个区间了

Q. task抛出异常,线程池中这个work thread还能运行其他任务吗?

A. 不能。 但是会创建新的线程, 新线程可以运行其他task。

对于 schedulerThreadPoolExecutor?   虽然有新线程,但是旧的循环任务不会再继续执行了, 开发实践推荐任务中捕获所有Exception

                                                                               如何合理配置线程数量

1)CPU密集型:

       定义:CPU密集型的意思就是该任务需要大量运算,而没有阻塞,CPU一直全速运行。
       CPU密集型任务只有在真正的多核CPU上才可能得到加速(通过多线程)。
       CPU密集型任务配置尽可能少的线程数。
       CPU密集型线程数配置公式:CPU核数+1个线程的线程池

2)IO密集型:

       定义:IO密集型,即该任务需要大量的IO,即大量的阻塞。
       在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力浪费在等待。
       所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要利用了被浪费掉的阻塞时间。
      
       第一种配置方式:
       由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程。
       配置公式:CPU核数 * 2。
       第二种配置方式:
       IO密集型时,大部分线程都阻塞,故需要多配置线程数。
       配置公式:CPU核数 / 1 – 阻塞系数(0.8~0.9之间)
       比如:8核 / (1 – 0.9) = 80个线程数
 

参考:https://www.cnblogs.com/yszzu/p/10122658.html

https://www.cnblogs.com/qingquanzi/p/8146638.html

https://blog.csdn.net/lixinkuan328/article/details/94501073

发布了740 篇原创文章 · 获赞 65 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41723615/article/details/104372959