线程池详解(从创建到源码)

什么是线程池

线程池是为了解决高并发多线程下面频繁创建线程,销毁线程,带来大量的线程调度的资源消耗问题的,也就是说有了线程池,来了一个任务,就不需要我们手动创建线程,而是将任务交给线程池去处理,这样就可以节省了大量的系统资源。

  1. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
    说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 --阿里巴巴开发手册

创建一个线程池

在java中有五个线程池的实现,
可以看到线程池ThreadPoolExecutor有四个构造方法,newScheduledThreadPool、newWorkStealingPool、newFixedThreadPool、、newCachedThreadPool、newCachedThreadPool。

package pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService fixedFhreadpool = Executors.newFixedThreadPool(5);
        ExecutorService singleThreadpool = Executors.newSingleThreadExecutor();
        ExecutorService cachedThreadpool = Executors.newCachedThreadPool();

        System.out.println("newFixedThreadPool===================");
        try {
            for (int i = 0; i < 10; i++) {
                fixedFhreadpool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");

                });

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            fixedFhreadpool.shutdown();
        }
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("singleThreadpool===================");
        try {
            for (int i = 0; i < 10; i++) {
                singleThreadpool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");

                });

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            singleThreadpool.shutdown();
        }
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("cachedThreadpool===================");
        try {
            for (int i = 0; i < 10; i++) {
                cachedThreadpool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");

                });

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            cachedThreadpool.shutdown();
        }


    }
}

在这里插入图片描述
上面是代码以及运行结果,这边演示了三种线程池的操作,其余两个newScheduledThreadPool是带任务调度的的线程池,newWorkStealingPool下面再讲。
跟踪到这三个方法的中。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
可以看到这三个方法用的是同一个构造方法,这里传入了五个参数,继续跟进这个ThreadPoolExecutor构造方法,
在这里插入图片描述
继续跟进
在这里插入图片描述
最终发现万变不离其宗,所有的线程池都是通过这个方法来构造的。

  1. corePoolSize //线程池初始线程数 银行默认开的窗口数
  2. maximumPoolSize //线程池最大线程数 银行最大窗口数
  3. keepAliveTime //线程关闭时间 银行临时窗口的等待关闭时间
  4. unit //时间单位
  5. workQueue //存放任务的队列 顾客的等待区
  6. threadFactory //线程工厂,一般用默认
  7. handler //拒绝策略 最大窗口满了,等待区满了,所采取的策略

怎么去理解这7个参数,这里引用阳哥的银行例子来举例。
在这里插入图片描述
银行现在默认有2个窗口处理业务,还有3个临时窗口不忙的时候是关闭的,
①正常处理业务2个窗口够了,
②后来的人就在等待区等待,·
③但是遇到高峰期,就需要紧急加3个窗口来处理业务,这个时候如果worker达到了最大,并且等待区也满了,并且还有外来顾客,
④那么就会启动拒绝策略,DiscardOldestPolicy-把队列头的任务(等待最久的)去掉,AbortPolicy-抛异常,CallerRunsPolicy-让发送任务的线程来执行任务,DiscardPolicy-什么都不做,无作为。
⑤接下来如果高峰期过了之后,3个紧急窗口超过一定的时间没有处理业务,就会关闭窗口。
对应的线程池的执行步骤。
在这里插入图片描述
再啰嗦一遍,这边是阳哥的资料。
在这里插入图片描述

newFixedThreadPool

这个时候再回到newFixedThreadPool,传入了一个固定线程数然后将初始化和最大线程数设置为nThreads,就是固定线程数,就是fixed,当然也就不需要等待时间,然后传入一个堵塞队列LinkedBlockingQueue,
因为是固定的,所以不需要打开关闭线程,性能很稳定,适用于执行长期任务。

newSingleThreadExecutor

线程池的初始大小为1,最大也为1,就是一次只能执行一个任务,也就是Single
适用于一个任务一个任务执行的的场景。

newCachedThreadPool

从名字来看是可缓存的,他的初始化是0个,最大值是int的最大值,也就相当于无限大
适用于执行很多短期的异步的小程序或加载较轻的服务

在这里插入图片描述
这几个都不被推荐使用,所以只能自定义了。

package pool;

import java.util.concurrent.*;

public class MyThreadPool {


    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        try {
            for (int i = 1; i <= 12; i++) {
                int finalI = i;
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务"+ finalI);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}

方法很简单使用这个就可以了ThreadPoolExecutor,这里定义了初始化为2,最大为5,等待时间2s,堵塞队列大小为5,拒绝策略DiscardOldestPolicy的一个线程池,可以看到执行之后任务3,4没有执行到,也就是说这两个任务被拒绝策略去掉了,
在这里插入图片描述
所以需要根据具体业务来设置这个值,也需要根据硬件情况来设置参数,一般如果是 CPU密集型的(需要大量的运算,CPU的使用密度很高),最大线程数一般设置为CPU核数+1,Runtime.getRuntime().availableProcessors()得到核数,如果是IO密集型(读取写入的操作比较多,传输比较多,读取数据库之类的)配置为CPU核数*2,CPU核数/(1-堵塞系数)

原理(从源码角度解释线程池工作过程)

先解释一下这里的几个参数(这里最好自己打开源码结合源码看参数,我只截图了主方法)

参数萌
  • commond是任务
  • ctl是一个原子整型(原子整型),ctl的高三位isRunning( c)表示线程池的五中状态,低28位workerCountOf( c)表示线程池中开启的线程的个数,get()方法就是拿到里面的int值
  • core 是否是核心线程
  • workerStarted
  • workerAdded
  • workers 是一个hashset里面存放worker
execute

这里几个判断

  1. 如果commond是空,抛异常
  2. workerCountOf( c)拿到当前启动的线程个数,如果小于corePoolSize核心线程数,就执行addWorker(command, true)(这个方法很重要下面重点讲)添加worker,并且把command放到添加的worker里面执行
  3. workQueue.offer(command)是将任务放入队列,没什么好讲的,返回是否成功,这边添加完成之后本来就可以完事了,但是接下来又做了一个验证,保证线程池正在运行。下面如果线程数是0,就执行addWorker(null, false),为什么传空呢?很重要!!!!
  4. 如果添加失败,拒绝策略处理任务

在这里插入图片描述

addWorker
addWorker
addWorker

retry:就是goto的意思,

  • line903进入for循环,line908判断是否再运行,检查队列是否是空
  • line914接下来for循环,line916判断如果开启线程数大于28位最大值,如果大于线程最大数,line918就不能创建,双重判断,接下来,line919compareAndIncrementWorkerCount( c)写时复制,将线程数+1添加成功,line920跳出两个for,line922失败line923CAS自旋再添加
  • 接下来就是添加线程阶段,line932新建一个worker,line933拿到线程,注意这里用的是final,line934如果t不是空,line936上锁,line943判断线程池的工作状态,并且确认firstTask不为空,line945然后再次检查t是否启动,再t启动之后把,line947worker添加到workers里面,line949再次检查worker的数量是否大于最大数量,如果大于就将最大数量设置为workers大小,line951标记workerAdded为true,添加成功,并且这边是把任务也放进了worker里面执行了的,至此addworker就完成了。

在这里插入图片描述
所以addWorker(command, true),这个逻辑就是正常添加worker,而addWorker(null, false)就是添加一个不带任务的woker
至此,上面的代码就完成了,初始化worker,并且创建线程把worker放入执行,来了任务将任务放入队列,以及队列满了之后的拒绝任务,接下来还需要找到,从队列中取出任务来执行,,这段逻辑

runWorker

这个方法从line1134开始看,task = getTask()是在while循环里面的,每次都从队列里面取出任务,里呢146beforeExecute(wt, task)是预留的一个空方法,执行之前的操作,line1149可以看到真正的执行任务的代码。也就是说某个woreker一但run起来,就会从队列中取出任务来执行,
在这里插入图片描述

getTask

还有一个逻辑,线程超时会自动回收,这段逻辑再getTask方法中,看到先进行一些判断,然后line1062开启线程数大于核心线程数的情况下为true,line1066自旋任务数减1,line1072timed是true,line1073会从队列中拉取一个任务并且设置当前线程的超时时间,如果时间默认为60s,如果超时那就结束当前线程,
在这里插入图片描述
至此,整个线程池的大概流程到这里就结束了。

总结

看完整个源码,感慨万分,jdk源码的设计中有太多太多可以学习的了,在使用对象前,一定要进行非空检查,这是一个习惯,这里初略的写完了线程池的 整个流程,以及实现原理,七大参数,当然线程池不止这些,还有很多是需要继续看源码学习的,看源码不仅仅是学习逻辑,设计思想,还有代码风格和编程习惯也可以学习学习,共勉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小丸子呢

致力于源码分析,期待您的激励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值