什么是线程池?
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。常见的运用池化思想的有:内存池、数据库连接池。使用线程池的优点如下:
1、降低资源的消耗:使得线程可以重复使用,不需要在创建线程和销毁线程上浪费资源
2、提高响应速度:任务到达时,线程可以不需要创建即可以执行
3、线程的可管理性:线程是稀缺资源,如果无限制的创建会严重影响系统效率,线程池可以对线程进行管理、监控、调优。
线程池执行流程
线程池工作流程
1.当有新的任务提交时,如果此时线程池中的任务小于线程池的核心线程数会直接分配一个核心线程处理
2.如果此时线程池中的任务大于线程池的核心线程数,会将任务放到阻塞队列里,如果核心线程处理完任务空闲了会去队列中去取任务执行
3.如果此时线程池中的任务大于线程池的核心线程数,会启动非核心线程来帮忙处理任务
4.如果非核心线程也满了,达到了线程池的最大线程数,就会执行拒绝策略
拒绝策略
ThreadPoolExecutor内部有实现4个拒绝策略:
(1)、CallerRunsPolicy,由调用execute方法提交任务的线程来执行这个任务;
(2)、AbortPolicy,抛出异常RejectedExecutionException拒绝提交任务;
(3)、DiscardPolicy,直接抛弃任务,不做任何处理;
(4)、DiscardOldestPolicy,去除任务队列中的第一个任务(最旧的),重新提交;
Excutor框架
Excutor框架是线程池处理线程的核心,包括创建任务,传递任务,任务的执行三个方面
Java自带的几种线程池详解
1.FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数和最大线程数相等,创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待
队列用的是 LinkedBlockingQueue,队列大小为Integer.MAX_VALUE,线程池没有拒绝任务的策略,当任务很多的时候,可能会产生OOM内存溢出
2.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到特点是只有一个核心线程,没有非核心线程,它可以保证先进先出的执行顺序
同样的其等待队列也是用的 LinkedBlockingQueue 无界队列,同样也可能出现 OOM。
3.newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
从源码可以看到,其核心线程数为0,但最大线程数目为 Integer.MAX_VALUE,即是无界,其只要来任务需要线程就会创建线程,极端情况会造成线程耗尽的情况
其执行过程是当有任务传递给线程池会先查看现有的线程是否够,如果不够,则会创建新的线程。 其缺点就是可能创建大量线程,导致OOM
4.newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
创建一个可以执行延迟任务的线程池
ScheduledThreadPoolExecutor:
schedule:延迟多长时间之后只执行一次;
scheduledAtFixedRate固定:延迟指定时间后执行一次,之后按照固定的时长周期执行;
scheduledWithFixedDelay非固定:延迟指定时间后执行一次,之后按照:上一次任务执行时长 + 周期的时长 的时间去周期执行;
5.newSingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池
7.newWorkStealingPool
创建一个抢占式执行的线程池(任务执行顺序不确定)JDK 1.8 中添加
线程池的线程数设置多少合适?
我们调整线程池中的线程数量的最主要的目的是为了充分并合理地使用 CPU 和内存等资 源,从而最大限度地提高程序的性能。在实际工作中,我们需要根据任务类型的不同选择对应 的策略。
CPU密集型任务
CPU密集型任务也叫计算密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗 费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的 线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以 上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是 满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这 就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多 会导致性能下降。
IO密集型任务
IO密集型任务,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特 别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数 一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果 我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当 一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在工作队列中等待的任务就会减少,可以更好地 利用资源。
线程数计算方法 《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程 数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随 之减少。 太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如 果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情 况衡量应该创建的线程数,合理并充分利用资源。