什么是线程池?
通俗理解就是一个容器,里面放了一些线程,需要用时就取出来用,用完了就放回去等待下一次用。
线程池内部维护一个任务队列,从池里取出线程去执行队列里的任务。
为什么要使用线程池?
1.可以将任务的提交和执行策略解耦,便于统一管理任务执行策略,好维护,比如延时执行,设置等待时间,超时自动失败等。
2.提高性能,用已创建的线程执行任务,减少创建和销毁线程的开销。
3.约束最大线程并发数,防止无止境创建线程造成性能变差以及程序死掉。
4.活跃线程数、最大线程数等参数可配置,方便进行性能调优。
如何创建线程池?
下图例子创建了一个核心线程数为10、最大线程数为30、非核心线程闲置超时时长为3毫秒的线程池,往线程池提交了三个任务。
Java线程池ThreadPoolExecutor实现了Executor接口,提供如下几个构造函数:
ThreadPoolExecutor构造函数各个参数的含义:
1.核心线程:int corePoolSize => 该线程池中核心线程数最大值
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程
核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉
正常情况下你不干活我也养你,因为我总有用到你的时候,但有时候特殊情况(比如我自己都养不起了),那你不干活我就要把你干掉了
2.线程池最大线程数: int maximumPoolSize 该线程池中线程总数最大值。线程总数 = 核心线程数 + 非核心线程数。
- 非核心线程闲置超时时长:****long keepAliveTime
该线程池中非核心线程闲置超时时长
一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
如果设置allowCoreThreadTimeOut = true,则会作用于核心线程
4.****时间单位:unit
指定keepAliveTime的时间单位,使用TimeUnit取值。
5.****工作队列:workQueue
该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
常用的workQueue类型:
- SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
- LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
- ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
- DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
ThreadPoolExecutor的策略
当一个任务被添加进线程池时:
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePools,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会由 RejectedExecutionHandler抛出异常
上面的构造参数太多太复杂,怕玩不转?
Concurrent包还提供了一套基于ThreadPoolExecutor的封装,使得使用者无需关注那么多配置细节,下图是Executors创建线程池的用法:
Executors创建的四种线程池:
1.可缓存线程池: CachedThreadPool
a.没有最大线程数限制,想创建多少就创建多少
b.有空闲线程就用空闲线程,没有就创建新线程
c.复用空闲线程能减少一部分重复创建销毁的开销
2. 定长线程池:FixedThreadPool
a.用来控制线程最大并发数。
b.如果没有空闲线程,剩下的任务会在队列等待有空闲线程执行。
3.支持定时和周期反复执行的定长线程池:ScheduledThreadPool
4.单线程的线程池:SingleThreadExecutor
线程池里只有一个线程,按照队列的出入规则来串行执行任务,例如先进先出。
线程池的大小设置多少合适?
线程池过大:大量线程竞争有限的cpu和内存资源,耗尽资源。
线程池过小:cpu利用率低,吞吐量低。
应当根据应用的特点来设计大小。
1.服务器的cpu多少?内存多大?任务是计算密集型还是IO密集型还是网络请求。
2.计算密集型的任务,在N个cpu的机器上大小为N或者N+1的线程池大小能实现最优使用率。
3.包含IO操作的任务或者IO密集型的任务,线程不会一直执行,会等待IO响应,可以把线程池设置得比N+1更大,用监控工具估算出计算任务和等待任务的比值,然后一步步微调,一步步试,看多大是性能最优的。
使用线程池的场景:
1、需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。
不使用线程池的场景:
1、如果需要使一个任务具有特定优先级
2、如果具有可能会长时间运行(并因此阻塞其他任务)的任务
3、如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)