一、线程池基本介绍
1、线程池
线程池:一个管理线程的池子。
2、为什么使用线程池?
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。
3、线程池执行原理
- 当线程池里存活的线程数小于核心线程数 corePoolSize 时,这时对于一个新提交的任务,线程池 会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数 corePoolSize 时, 线程池里面的线程会一直存活着,就算空闲时间超过了 keepAliveTime ,线程也不会被销毁,而 是一直阻塞在那里一直等待任务队列的任务来执行。
- 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务 队列workQueue排队等待执行。
- 当线程池里面存活的线程数已经等于 corePoolSize 了,并且任务队列也满了,假设 maximumPoolSize>corePoolSize ,这时如果再来新的任务,线程池就会继续创建新的线程来处 理新的任务,知道线程数达到 maximumPoolSize ,就不会再创建了。 4. 如果当前的线程数达到了 maximumPoolSize ,并且任务队列也满了,如果还有新的任务过来,那 就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。
二、线程池的使用
1.基本线程池种类
常见的线程池有 FixedThreadPool 、 SingleThreadExecutor 、 CachedThreadPool 和 ScheduledThreadPool 。这几个都是 ExecutorService 线程池实例。
- FixedThreadPool 固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务,使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE)在任务比较多的时 候会导致 OOM。
- SingleThreadExecutor 只有一个线程的线程池,使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE)在任务比较多的时 候会导致 OOM。
- CachedThreadPool 根据需要创建新线程的线程池。CachedThreadPool 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
- ScheduledThreadPoolExecutor 在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比 如 quartz 。
2、线程池大小怎么设置?
如果线程池线程数量太小,当有大量请求需要处理,系统响应比较慢,会影响用户体验,甚至会出现任 务队列大量堆积任务导致OOM。 如果线程池线程数量过大,大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而增 加线程的执行时间,影响了执行效率。
-
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1 , 多出来的一个线程是为了防止某些原因导致的线程阻塞(如IO操作,线程sleep,等待锁)而带来的影 响。一旦某个线程被阻塞,释放了CPU资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的 空闲时间。
-
I/O 密集型任务(2N): 系统的大部分时间都在处理 IO 操作,此时线程可能会被阻塞,释放CPU资源, 这时就可以将 CPU 交出给其它线程使用。因此在 IO 密集型任务的应用中,可以多配置一些线程,具体 的计算方法: 最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (IO耗时/CPU耗时)) , 一般可设置为2N。
这些是理论上的配置,实际开发中有条件的话,应根据压测情况配置实际参数值。
3、实际使用的线程池ThreadPoolExecutor
ThreadPoolExecutor 的通用构造函数:
-
corePoolSize :当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线 程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑 调大 corePoolSize。
-
maximumPoolSize :当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的 线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时 间超过 keepAliveTime 之后,就应该退出,避免资源浪费。
-
BlockingQueue :存储等待运行的任务。
-
keepAliveTime :非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0, 表示多余的空闲线程会被立即终止。
-
TimeUnit :时间单位
-
拒绝策略RejectedExecutionHandler
AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务
面试题
1、进程线程的概念和区别?
-
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。
-
线程是比进程更小的执行单位,它是在一个进程中独立的控制流,一个进程可以启动多个线程,每条线 程并行执行不同的任务。
2、创建线程有哪几种方式?
- 通过扩展 Thread 类来创建多线程
- 通过实现 Runnable 接口来创建多线程
- 实现 Callable 接口,通过 FutureTask 接口创建线程。
- 使用 Executor 框架来创建线程池