java多线程:线程池原理、阻塞队列
⼀、线程池定义和使⽤
jdk1.5之后就引⼊了线程池。
1.1定义
从上⾯的空间切换看得出来,线程是稀缺资源,它的创建与销毁是⼀个相对偏重且耗资源的操作,⽽Java线程依赖于内核线程,创建线程需要进⾏操作系统状态切换。为避免资源过
度消耗需要设法重⽤线程执⾏多个任务。线程池就是⼀个线程缓存,负责对线程进⾏统⼀分配、调优与监控。(数据库连接池也是⼀样的道理)
什么时候使⽤线程池?
单个任务处理时间⽐较短;需要处理的任务数量很⼤。
线程池优势?
重⽤存在的线程,减少线程创建、消亡的开销,提⾼性能、提⾼响应速度。
当任务到达时,任务可以不需要等到线程创建就能⽴即执⾏。
提⾼线程的可管理性,可统⼀分配,调优和监控。
1.2线程池在jdk已有的实现
在juc包下,有⼀个接⼝:Executor:
Executor⼜有两个⼦接⼝:ExecutorService和ScheduledExecutorService,常⽤的接⼝是ExecutorService。
同时常⽤的线程池的⼯具类叫Executors。
例如:
ExecutorServicervice=hedThreadPool();
Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()、newScheduledThreadPool()等创建线程池的⽅法,但都有其局限
性,不够灵活。
上⾯的⼏种⽅式点进去会发现,都是⽤ThreadPoolExecutor进⾏创建的:
newSingleThreadExecutor字⾯意思简单线程执⾏器。
newFixedThreadPool字⾯意思固定的线程池,传参就是线程固定数⽬,适⽤于执⾏长期任务的场景。
newCachedThreadPool字⾯意思缓存线程池,核⼼线程0,最⼤线程⾮常⼤,动态创建的特点。
newScheduledThreadPool字⾯意思时间安排线程池,指定核⼼线程数。
newSingleThreadScheduledExecutor字⾯意思单线程安排执⾏器,也就是基于只有⼀个核⼼线程的执⾏器之外,⼜可以扩展。其中⼜⽤DelegatedExecutorService委托执⾏器服务进
⾏了包装。
可以看到,上⾯直接⽤Executors⼯具类默认的⼀些实现new出来的线程池都是⽤的ThreadPoolExecutor线程执⾏器这个类进⾏构造的,不过参数不同,导致了效果的侧重点不
同。
因此,⾃⼰创建线程池推荐的⽅法就是,直接使⽤ThreadPoolExecutor进⾏个性化的创建:
构造⽅法种的参数有7个:
corePoolSize:线程池维护线程的最少数量(core:核⼼)
maximumPoolSize:线程池维护线程的最⼤数量,显然必须>=1
keepAliveTime:线程池维护的多余的线程所允许的空闲时间,最长可以空闲多久,时间到了,如果超过corePoolSize的线程⼀直空闲,他们就会被销毁。
unit:线程池维护线程所允许的空闲时间的单位
workQueue:线程池所使⽤的缓冲队列,已经提交但是没有执⾏的任务会放进这⾥
threadFactory:⽣成线程池种⼯作线程的线程⼯⼚,⼀般使⽤默认
handler:线程池对拒绝任务的处理策略,当队列满且⼯作线程已经达到maximumPoolSize。
阿⾥的java开发⼿册,强制要求,通过ThreadPoolExecutor来⾃定义,不能使⽤内置的,避免资源耗尽。这个很好理解,1的类型就只有⼀个核⼼线程和最⼤现场,2没有扩展
性,3、4、5的最⼤线程数太⼤,内存会爆炸。
1.3线程池使⽤⽅法
这⾥我们⽤固定线程池来测试,传⼊核⼼线程数为5,最⼤数量⾃然就也是5,
publicstaticvoidmain(String[]args){
ExecutorServicethreadPool=edThreadPool(5);
try{
//模拟10个顾客办理业务
for(inti=0;i<10;i++){
//execute执⾏⽅法,传⼊参数为实现了Runnable接⼝的类
e(()->{
n(tThread().getName()+"号线程办理业务");
});
}
}catch(Exceptione){
tackTrace();
}finally{
wn();
}
}
其中,execute⽅法就是将任务提交的⽅法,我们⽤lambda表达式给execute⽅法传⼊了参数,实际上相当于⼀个完整的实现了Runnable接⼝的类。
执⾏结果:
可以看到,我们循环了10次,执⾏任务,但是线程只⽤到了1-5,其中有多次复⽤。
再⽐如,我们按照各种类型的线程池,⾃⼰定义⼀个线程池,核⼼线程数2,最⼤线程数5,阻塞队列长度为3:
publicstaticvoidmain(String[]args){
ExecutorServicethreadPool=newThreadPoolExecutor(
2,
5,
2L,
S,
newLinkedBlockingDeque<>(3),
tThreadFactory(),
olicy()
);
try{
//模拟10个顾客办理业务
for(inti=0;i<10;i++){
//execute执⾏⽅法,传⼊参数为实现了Runnable接⼝的类
e(()->{
n(tThread().getName()+"号线程办理业务");
});
}
}catch(Exceptione){
tackTrace();
}finally{
wn();
}
}
同样10个线程,执⾏起来:
可以看到,执⾏了8个任务后,就抛出了异常,说明执⾏了拒绝策略。
上⾯两个⽰例,我们的任务本⾝都是没有返回值的,如果创建的任务本⾝需要有返回值就需要实现Callable接⼝,然后搭配FutureTask来传⼊任务,那
么线程池就应该调⽤submit⽅法⽽不是execute。
⼆、线程池底层原理
2.1线程池执⾏逻辑
处理的流程核⼼就execute()⽅法,他接收⼀个实现了Runnable接⼝的任务,决定对这个任务的处理策略。
下图是⼀个⽐较形象的策略流程:
可能的情况有四种,也就是图中的1234:
如果线程池中的线程数量少于corePoolSize,就创建新的核⼼线程来执⾏新添加的任务
如果线程池中的线程数量⼤于等于corePoolSize,但队列workQueue未满,则将新添加的任务放到队列workQueue中
如果线程池中的线程数量⼤于等于corePoolSize,且队列workQueue已满,但线程池中的线程数量⼩于maximumPoolSize,则会创建新的⾮核⼼线程来处理被添加的任务
如果线程池中的线程数量等于了maximumPoolSize,就⽤RejectedExecutionHandler来执⾏拒绝策略。会抛出异常,⼀般的拒绝策略是RejectedExecutionException
注意,执⾏的顺序,在java⾥有⼀个不合理的地⽅:
在池⾥安排任务的时候,我们的核⼼线程,队列,⾮核⼼线程⾥⾯排的任务顺序应该是123;
但是真正实现上,如果三个都满了,开始执⾏的时候,依次执⾏的顺序却是核⼼线程,⾮核⼼线程,队列。也就是执⾏顺序会变成132
2.2拒绝策略
有些时候,我们并不希望拒绝策略是直接抛出异常,那么jdk⾥⾯提供的默认拒绝策略有4种,他们体现在代码中就是ThreadPoolExecutor的四个静态内部类:
2.2.1CallerRunsPolicy:调⽤者运⾏策略。
这种策略不会抛弃任务,也不抛出异常,⽽是将某些任务回退给调⽤者,从⽽降低新任务的流量。
实现⾮常简单,那就是如果说e这个线程池已经shutdown了,那么就什么也不⼲,也就是这个任务直接丢了;否则,(),相当于调⽤这个⽅法的线程⾥直接执⾏了这个
Runnable任务。
此时我们可以把1.3⾥的代码修改⼀下,只修改策略为CallerRunsPolicy:
可以看到,有些任务会在main线程⾥处理。
2.2.2AbortPolicy:终⽌策略。
抛异常。前⾯已经试过了,这个是默认的拒绝策略。
2.2.3DiscardPolicy:丢弃任务。
可以看到,源码⾥就是是什么也不做。如果场景中允许任务丢失,这个是最好的策略。
2.2.4DiscardOldestPolicy:抛弃队列中等待最久的任务。
抛弃队列中等待最久的任务,然后把当前的任务加⼊队列中,尝试再次提交当前任务。
源码⾥也就是利⽤队列操作,进⾏⼀次出队操作,然后重新调⽤execute⽅法。
2.3线程池的五种状态
和⼀个正常的线程的⽣命周期区别开,这个是线程池⾥线程的状态。
Running,能接受新任务以及处理已添加的任务;
Shutdown,不接受新任务,可以处理已经添加的任务,也就是不能再调⽤execute或者submit了;
Stop,不接受新任务,不处理已经添加的任务,并且中断正在处理的任务;
Tidying,所有的任务已经终⽌,CTL记录的任务数量为0,CTL负责记录线程池的运⾏状态与活动线程数量;
Terminated,线程池彻底终⽌,则线程池转变为terminated的状态。
如图所⽰,从running状态转换为shutdown,调⽤shutdown()⽅法;如果调⽤shutdownNow()⽅法,就直接会变成stop。
terminated()是钩⼦函数,默认是什么也不做的,我们可以重写,然后决定结束之前要做⼀些别的处理逻辑。这个钩⼦函数,就是模板模式的⽅法。
三、阻塞队列
线程池⾥的BlockingQueue,阻塞队列,事实上在消费者⽣产者问题⾥的管程法实现,我们的策略也是类似阻塞队列的,⽤它来做⼀个缓存池的作⽤。
阻塞队列:任意时刻,不管并发有多⾼,永远保证只有⼀个线程能够进⾏队列的⼊队或出队操作。也就意味着他是能够保证线程安全的。
另外,阻塞队列分为有界和⽆界队列,理论上来说⼀个是队列的size有固定,另⼀个是⽆界的。对于有界队列来说,如果队列存满,只能出队了,⼊队操作就只能阻塞。
在juc包⾥,阻塞队列的实现有很多:
ArrayBlockingQueue:有界阻塞队列;
LinkedBlockingQueue:链表结构(⼤⼩默认值为_VALUE)的阻塞队列;
PriorityBlockingQueue:⽀持优先级排序的⽆界阻塞队列;
DelayQueue:使⽤优先级队列实现的延迟⽆界阻塞队列;
SynchronousQueue:不存储元素的阻塞队列,相当于只有⼀个元素;
LinkedTransferQueue:链表组成的⽆界阻塞队列;
LinkedBlockingDeque:链表组成的双向阻塞队列。
对于BlockingQueue来说,核⼼操作主要有⼏类:插⼊、删除、查找。
其中的四种异常策略:
抛异常:如果阻塞队列满,再往队列⾥add插⼊元素会抛IllegalStateException:Queuefull,如果阻塞队列空,再remove就会抛NoSuchElementException。
特殊值:offer⽅法:成功true,失败fal,poll⽅法,成功就返回元素,没有就返回null。
阻塞:阻塞队列满的时候,⽣产者线程继续put元素,队列就会阻塞直到可以put数据或者响应中断然后退出,阻塞队列空的时候,消费者线程继续take元素,队列就会⼀直阻塞
直到有元素可以take。
超时退出:阻塞队列满的时候,会阻塞⽣产者线程且超时退出,空的时候会阻塞消费者线程且超时退出。
那么使⽤的时候,增删的⽅法按对应的同⼀组使⽤⽐较合理。(其实这个策略的设计对应的在单线程集合⾥也有,那就是Deque接⼝的实现类LinkedList使⽤的时候,不同的增删⽅
法策略不同)
本文发布于:2022-11-25 00:22:03,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/15176.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |