学习参考资料:《Java并发编程的艺术》
1、为什么要使用线程池(优点)
- 避免重复的进行线程的创建和摧毁,可以减少不必要的资源开销;
- 任务到达后,可以直接获取线程执行,提高了响应速度;
- 可以交给线程池统一进行管理,提高线程的可管理性;
2、线程池的实现原理
当向线程池提交一个任务时,线程池是怎样处理这个任务的呢?
ThreadPoolExecutor主要执行execute()
来实现的,如下图:
1)看线程池中的线程数量是否达到核心线程数;没有就创建线程执行任务,否则继续执行 2
2)将任务添加到阻塞队列中,等待线程处理;如果阻塞队列已经满了,执行 3
3)如果线程池中线程的数量没有超过最大线程数,就创建线程执行任务,否则执行 4
4)执行提前设置好的饱和策略
注意:
- 线程在执行完当前任务时,会不断从阻塞队列中获取任务来执行
- 当一个线程空闲了一定时间&&线程池中线程数>核心线程数就会被摧毁
3、线程池的创建以及核心参数
我们可以通过ThreadPoolExecutor
来创建一个线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
线程池的七个参数
corePoolSize
:核心线程数。maximumPoolSize
:最大线程数。keepAliveTime
:线程存活时间。当线程数大于core数,那么超过该时间的线程将会被终结。unit
:keepAliveTime的单位。java.util.concurrent.TimeUnit类存在静态静态属性: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDSworkQueue
:Runnable的阻塞队列。若线程池已经被占满,则该队列用于存放无法再放入线程池中的Runnable。threadFactory
:创建一个新线程时使用的工厂,可以用来设定线程名、是否为守护线程等等handler
:拒绝策略
4、如何合理地创建线程池
要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
- 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
- 任务的优先级:高、中、低。
- 任务的执行时间:长、中、短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接等。
性质不同的任务可以交给不同规模的线程池执行。
对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,可以拆分成IO密集型和CPU密集型分别处理。
若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
在《java并发编程实践》中有一个估算最佳线程数合理值的公式:
Nthreads=Ncpu*Ucpu*(1+w/c)
其中
Ncpu=CPU核心数
Ucpu=cpu使用率,0~1
W/C=等待时间与计算时间的比率
IO密集型
:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu。这样设置一般都OK。
计算密集型
:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。
上面只是给了大概的配置线程池的参考,但是如果在实际开发中要具体确定合理地线程池值大小,还是需要结合系统实际情况,通过压力测试来进行微调。