一. 线程池简介
1. 线程池的概念:
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
2. 线程池的工作机制
2.1 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
2.1 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
3. 使用线程池的原因:
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。
二. ExecutorService常见的四种线程池详解
1. 线程池的返回值ExecutorService简介:
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
2. 具体的4种常用的线程池实现如下:(返回值都是ExecutorService)
2.1 Executors.newFixedThreadPool(int n):
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
* 可控制线程最大并发数,超出的线程会在队列中等待
* 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
* 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
* CUP密集型:N+1
* IO密集型:2N+1
2.2 Executors.newCacheThreadPool():
可缓存线程池,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。缓存 型池子通常用于执行一些生存期很短的异步型任务
2.3 Executors.newScheduledThreadPool(int n):
创建一个定长线程池,支持定时及周期性任务执行
2.4 Executors.newSingleThreadExecutor():
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
3.ThreadPoolExecutor的重要参数
- corePoolSize:核心线程数
-
- 核心线程会一直存活,及时没有任务需要执行
- 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
- 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
- queueCapacity:任务队列容量(阻塞队列)
-
- 当核心线程数达到最大时,新任务会放在队列中排队等待执行
- maxPoolSize:最大线程数
-
- 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
- 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
- keepAliveTime:线程空闲时间
-
- 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
- 如果allowCoreThreadTimeout=true,则会直到线程数量=0
- allowCoreThreadTimeout:允许核心线程超时
- rejectedExecutionHandler:任务拒绝处理器
-
- 两种情况会拒绝处理任务:
-
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
- 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
- ThreadPoolExecutor类有几个内部实现类来处理这类情况:
-
- AbortPolicy 丢弃任务,抛运行时异常,默认策略
- CallerRunsPolicy 直接执行任务
- DiscardPolicy 忽视,什么都不做
- DiscardOldestPolicy 从队列中poll出最靠前的任务,并且执行当前任务
- 实现RejectedExecutionHandler接口,可自定义处理器
4、ThreadPoolExecutor执行顺序:
线程池按以下行为执行任务
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满
- 若线程数小于最大线程数,创建线程
- 若线程数等于最大线程数,抛出异常,拒绝任务
三. spring的线程池-ThreadPoolTaskExecutor
private ThreadPoolExecutor threadPoolExecutor;
可见ThreadPoolTaskExecutor底层使用是使用的ExecutorService
这是ThreadPoolTaskExecutor用来初始化threadPoolExecutor的方法,BlockingQueue是一个阻塞队列,ThreadPoolTaskExecutor的实现方式完全是使用threadPoolExecutor进行实现的。
四.异步非阻塞获取并行任务执行结果的线程池-CompletionService
Callable,Future用于返回结果
Future<V>代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则,get()会使当前线程阻塞。FutureTask<V>实现了Future<V>和Runable<V>。Callable代表一个有返回值得操作。使用Future和Callable可以获取线程执行结果,但获取方式确实阻塞的,根据添加到线程池中的线程顺序,依次获取,获取不到就阻塞。为了解决这种情况,可以采用轮询的做法。
//初始化线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
获取的结果是无序的,第三个睡眠线程始终最后结束,也证实了获取结果的方式是非阻塞的。如下:
1)源码分析
首先看一下 构造方法:
构造法方法主要初始化了一个阻塞队列,用来存储已完成的task任务。
然后看一下 completionService.submit 方法:
可以看到,callable任务被包装成QueueingFuture,而 QueueingFuture是 FutureTask的子类,所以最终执行了FutureTask中的run()方法。来看一下该方法:
可以看到在该 FutureTask 中执行run方法,最终回调自定义的callable中的call方法,执行结束之后,通过 set(result) 处理执行结果:
继续跟进finishCompletion()
方法,在该方法中找到 done()
方法:
可以看到该方法只做了一件事情,就是将执行结束的task添加到了队列中,只要队列中有元素,我们调用take()方法时就可以获得执行的结果。
到这里就已经清晰了,异步非阻塞获取执行结果的实现原理其实就是通过队列来实现的,FutureTask将执行结果放到队列中,先进先出,线程执行结束的顺序就是获取结果的顺序