多线程高并发学习之线程池从入门到入土
临近过年,我又开始放纵自己了,托更好几天,今天中午12点半刚回到老家,带着我的狗子,今天是过年之前最后一个集,大街上可热闹了,我中午吃了饭去外边逛了逛,逛了一圈,想起我已经鸽了好几天了,于是我良心发现,赶紧回到家,打开电脑,开始肝文章。
废话不多说,今天来跟大家讲讲线程池从入门到入土
在讲线程池之前先说几个相关的类
线程池入门前需了解的类
Executor
Executor是一个接口,只有一个execute方法叫执行,也就是说你可以指定一个线程去执行他的run方法
ExecutorService
ExecutorService也是一个接口,它实现了Executor接口,并且又增加了很多的方法,比如shutdownNow马上结束、isShutdown是否结束、submin提交执行(丢给线程池,异步执行)等等比较常用的方法
Callable
Callable相当于是Runnable,Callable的call方法相当于Runnable的run方法,唯一不同的是call方法有返回值,而run方法没有返回值
Future
Future是什么呢?Future其实就是上面讲的Callable执行call方法返回的结果,将会被封装到Future里面,ExecutorService的sumin方法,返回值类型就是Future,当调用Future的get方法,将会阻塞住,执行完毕并返回
- 在这插一个比较好玩的Future,叫做FutureTask,它既是一个事件,又是一个结果集,相当于一个有返回值的Runnable与Future的结合体,但是需要new一个Thread去帮他执行,如下图
那这个时候肯定有小伙伴们就要问了:啊,那有没有管理多个任务的啊?
听到这,我门头一咒,猛嘬一口凉白开,拍板叫道:当然有!
接下来隆重登场的就是CompletableFuture
这是个啥玩意呢?CompletableFuture有一个静态阻塞方法allOf,可以管理多个任务。
兄弟萌,请看,我写了3个方法,priceofTM、priceofJD、priceofPDD,都是睡不同秒钟,然后让3个CompletableFuture去执行,然后交给一个CompletableFuture去管理,调用这个allOf方法阻塞,直到所有方法执行完毕,它还有一个anyOf是当有一个执行完毕就解除阻塞
ok开胃菜吃完,现在我们来干硬菜!MingLog!上硬菜!
线程池从入门到入土
线程池的参数
corePoolSize核心线程数:顾名思义,就是核心线程的个数
maximumPoolSize最大线程数:能够允许存在的线程数(核心线程+非核心线程)
keepAliveTime生存时间:当非核心线程超过这个时间没有执行任务,就会被剔除掉,归还给操作系统
TimeUnit生存时间的单位:时间单位时、分、秒
workQueue任务队列:各种各样的BlockingQueue
threadFactory线程工厂:产生现成的工厂,需要实现ThreadFactory接口
handler拒绝策略:核心线程+非核心线程=最大线程数时,再申请线程时,执行拒绝策略
线程池执行流程
当线程池初始化后,其实里面是没有线程的,当要去执行一个任务时,创建一个核心线程去执行,再来一个任务,再去创建一个核心线程
直到创建达到核心线程个数之后再去创建非核心线程时需要检查核心线程有没有空闲的
如果有空闲的核心线程,则将这个任务交给核心线程去执行
如果没有空闲的核心线程(核心线程都在忙),则进入队列等着,等着核心线程忙完
如果等待队列满了,则创建非核心线程去执行任务
如果非核心线程没有执行任务超过keepAliveTime生存时间,则将这个非核心线程剔除掉交还给操作系统
如果核心线程+非核心线程=最大线程数,并且所有线程都在忙,没有空闲的,则再来任务需要创建线程时,直接执行拒绝策略
这里多bb几句,上述参数有一个等待队列BlockingQueue,常用的BlockingQueue的实现类不多,经过我种种测试,就有了以下结果,小伙伴们仔细看
- 如果队列选择LinkedBlockingQueue,我们知道它是无界队列,可以存放Integer最大值的任务,所以你的任务队列很有很能会越堆越多,而且只有核心线程在工作,因为只有任务队列满了才会开启非核心线程
- 如果队列选择ArrayBlockingQueue,这个是比较正常的,需要给定一个任务队列的长度,当任务堆积到阈值时,开启非核心线程,执行队列外的任务
- 如果队列选择DelayQueue,上回书说到,这玩意是以时间进行排序的,所以也是按任务创建的时间大小来执行的,具体看你重写的compareTo方法,但是!重点来了!这玩意也是个无界的,所以相当于是LinkedBlockingQueue+时间排序
- 如果队列选择SynchronusQueue,大家知道这玩意长度为0,所以,只能是一个个的执行,经过测试,如果你的线程池的线程达到了你设置的阈值,再添加任务时,他就会报错啦,因为只有当前任务被线程取走时才能继续往里添加任务
- 如果队列选择TransferQueue,效果和LinkedBlockingQueue一毛一样
线程池的拒绝策略
如果有人问你,线程池的拒绝策略有几种呢?
低情商回答:4种!
高情商回答:JDK默认提供的有4种,但是我们可以自定义拒绝策略!
好家伙,面试官直呼内行呐
JDK默认提供的拒绝策略有四种:AbortPolicy、DiscardOldestPolicy、DiscardPolicy、CallerRunsPolicy
- AbortPolicy:简单暴力,直接报错
- DiscardOldestPolicy:直接把队列的第一个任务给干掉,然后把新任务加进来,我直接好家伙,那任务不就丢了吗,慎用慎用
- DiscardPolicy:表面上啥都没干,其实它背地里把你要添加的新任务给偷偷干掉了
- CallerRunsPolicy:将你要添加的这个任务交给main线程去执行
常见的几种线程池
下面给大家介绍几种常见的线程池,面试给面试官说说,面试官直呼内行!
CachedPool
来来来,大家来看他的构造就知道啦,这玩意没有核心线程,而最大线程数为Integer.MAX_VALUE,存活时间时60S,队列用的是SynchronousQueue,想想这几个组合在一起是什么效果呢?来一个任务就开启一个线程(前提是线程池里没有空闲的线程),线程空闲60S就归还,循环往复
SingleThreadExecutor
来,在看这个好玩的,乍一看,我敲!一个核心线程,最大线程也是一,那不就是永远只有一个线程嘛,存活时间0,用的LinkedBlockingQueue作为队列,一个一个排好队来执行
FixedThreadPool
FixedThreadPool,创建的时候给定一个int值作为核心线程数以及最大线程数,嘛意思,就是说池子里全是核心线程,来一个我启动一个,直到达到阈值,然后其他任务进队列等着,等有核心线程忙完再去执行别的任务,排好队,一个一个来
具体项目实战中,这个线程数设置多少合理呢?这个东西应该针对项目而定
如果你项目属于CPU密集型(NIO,只进行一些计算运算,不参与磁盘),那么线程数约等于你服务器的CPU核数
如果你项目属于IO密集型(运算少,读写磁盘多),那么线程数设为CPU核数的10倍左右差不多
这东西是有公式的,我忘记了,具体设置好还是要进行压测直到找到最合适的那个线程数,一切以压测数据为准
自定义拒绝策略
自定义拒绝策略就很灵活了,根据自己的业务来定,我就直接伪代码了
另类的几个线程池
咱们上边说的都是ThreadPoolExecutor类型的,下面介绍几个ForkJoinPool类型的
WorkStealingPool
构造是直接new了一个ForkJoinPool,所以它本质其实就是ForkJoinPool
ForkJoinPool是每个线程都有属于自己的任务队列,每次都去自己的任务队列中取任务,当有的线程把自己队列的任务执行完之后,会去别的任务队列的屁股上去”偷“任务,不讲码德,马保国直呼内行
ForkJoinPool是将大任务进行分解,分解到什么程度呢?直到满足ForkJoinPool自定的那个条件,不满足就继续劈两半分解
好啦,就说这么多,看完了,同学们有没有入土呢,还没入土,就去看看ThreadPoolExecutor的源码,理解的更深,这篇文章肝了3天,不是我low,是回来家把狗子带回来,放楼下怕他乱吃,我不放心,在我屋里,他老是打扰我,哎,太难了。。。
看在我这么辛苦更新的份上,小伙伴们就不要白嫖了嘛