目录
3.1获取单一线程的线程池 newSingleThreadExecutor()
3.2获取固定数量线程的线程池 newFixedThreadPool(int nThreads)
3.3获取线程数量可伸缩的线程池 Executors.newCachedThreadPool()
1、线程池是什么?
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
2、为什么要使用线程池?
创建一个线程需要调用操作系统内核的 API,操作系统要为线程分配资源,CPU消耗成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
使用线程池就能很好地避免频繁创建和销毁。
基本的思想就是:之前的操作是需要线程去执行任务的时候去新建一个线程,执行完任务后,再销毁线程,十分的浪费资源。现在是在使用线程之前维护好一个有一个或多个线程的池子,在需要执行任务的时候就取一个线程去执行,执行完之后,再归还给线程池,这样可以实现资源的再利用。
作用:
- 降低资源的消耗
- 提高响应速度
- 便于管理线程
- 线程复用,可以控制最大并发数
3、JUC中线程池的三大线程池
3.1获取单一线程的线程池 newSingleThreadExecutor()
newSingleThreadExecutor();顾名思义,这个线程池中只有一个线程去执行任务
//1、获取单一线程池,即线程池中只有一个线程在工作
ExecutorService executorService = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程池对象有一个execute()方法,其实可以理解为往线程池中提交任务,参数是一个Runnable
void execute(Runnable command);
执行以下代码:
public class PoolTest {
public static void main(String[] args) {
//1、获取单一线程池,即线程池中只有一个线程在工作
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
for (int i = 1; i <=6 ; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();//使用完一定要释放线程池资源
}
}
}
结果:
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
由此可见,执行不同任务的其实都是一个线程
3.2获取固定数量线程的线程池 newFixedThreadPool(int nThreads)
//nThreads代表线程的数量
newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//2、获取固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
try {
for (int i = 1; i <=5 ; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();//使用完一定要释放线程池资源
}
执行结果:可见有五个线程执行任务,为我们指定的固定长度
pool-1-thread-1执行了
pool-1-thread-2执行了
pool-1-thread-3执行了
pool-1-thread-4执行了
pool-1-thread-5执行了
3.3获取线程数量可伸缩的线程池 Executors.newCachedThreadPool()
//3、线程数量可伸缩的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
执行测试一
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 1; i <=5 ; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();//使用完一定要释放线程池资源
}
测试一结果:
pool-1-thread-1执行了
pool-1-thread-2执行了
pool-1-thread-4执行了
pool-1-thread-3执行了
pool-1-thread-5执行了
然后我们发现当有5个任务的时候,线程池中有5个线程来执行它们
然后我们将任务数改为100的时候,发现有40个线程执行了任务。newCachedThreadPool()获取的是一个线程数不固定的线程池,后面我们将原理。
4、获取线程池的7大参数
前面我列出了获取每种线程池的方法的源码,最终我们都发现它们都new了同一个类(ThreadPoolExecutor)的对象
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
查看它的源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
..............
}
我们从源码中发现构建一个新的线程池对象有七个参数
- corePoolSize:核心线程数(最小线程数),一创建默认会有的线程数
- maximumPoolSize:最大线程数,当线程池中核心线程数都在执行任务时,会新建线程执行,最大的数目不会超过最大线程数
- keepAliveTime:线程(非核心线程,核心线程不会被销毁)执行完任务后多长时间没有再被使用,就会被销毁。
- unit:TimeUnit类型,指的是keepAliveTime的单位,时分秒等
- workQueue:阻塞对列
- threadFactory:线程工厂(一般自定义线程池的时候这个参数也不会动)
- handler:拒绝策略(有四种拒绝策略,后面会说,其实拒绝策略就是在超过最大线程数的情况下,还有任务提交,并且没有空闲的线程可用的情况下的处理情况)
这样我们明白了创建线程池的参数后,我们就发现
- newSingleThreadExecutor()的核心线程数和最大线程数都是1,阻塞对列为
默认大小的LinkedBlockingQueue<Runnable>()阻塞队列的长度为Integer.MAX_VALUE,所以可能因为阻塞队列过长而发生OOM
- newFixedThreadPool(int nThreads)的核心线程数和最大线程数都是传入的参数nThreads,而且他也有newSingleThreadExecutor的阻塞队列过长而导致OOM的可能。
- newCachedThreadPool()的核心线程数是0,而最大线程数是Integer.MAX_VALUE,所以这个线程池在很大的并发量下可能会因为创建大量线程而发生OOM,阻塞对列是new SynchronousQueue<Runnable>()同步阻塞队列
所以我们在使用线程池的时候,最好不要直接用Executors获取,最好是直接使用ThreadPoolExecutor获取自定义的线程池对象。
5、自定义一个线程池
自定义一个线程池,并且测试当任务数超过最大线程数+阻塞队列长度时,默认的拒绝策略会怎么做??
//自定义一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,//核心线程数
5,//最大线程数
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),//长度为3阻塞队列
Executors.defaultThreadFactory(),//默认线程工厂
new ThreadPoolExecutor.AbortPolicy()//默认的拒绝策略AbortPolicy()不处理并且抛出异常
);
try {
for (int i = 1; i <=9 ; i++) {
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPoolExecutor.shutdown();//使用完一定要释放线程池资源
}
执行结果:
pool-1-thread-1执行了
pool-1-thread-3执行了
pool-1-thread-3执行了
pool-1-thread-3执行了
pool-1-thread-4执行了
pool-1-thread-5执行了
pool-1-thread-2执行了
pool-1-thread-1执行了
java.util.concurrent.RejectedExecutionException: Task juctest.PoolTest$$Lambda$1/1096979270@7ba4f24f rejected from java.util.concurrent.ThreadPoolExecutor@3b9a45b3[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at juctest.PoolTest.main(PoolTest.java:29)
结果可见,当我们自定义一个线程池并且使用AbortPolicy()拒绝策略的时候,阻塞队列满了的时候,丢弃任务,并且抛出异常。
其实我们的拒绝策略RejectedExecutionHandler接口有四个实现类,也就是四种拒绝策略
6、四种拒绝策略
6.1AbortPolicy()
丢弃任务,并且抛出异常
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
6.2DiscardPolicy()
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃
6.3CallerRunsPolicy()
谁提交的任务,就交给哪个线程执行(谁叫你来的去找谁)
测试:
//自定义一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,//核心线程数
5,//最大线程数
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),//长度为3阻塞队列
Executors.defaultThreadFactory(),//默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy()//默认的拒绝策略AbortPolicy()不处理并且抛出异常
);
try {
for (int i = 1; i <=9 ; i++) {
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPoolExecutor.shutdown();//使用完一定要释放线程池资源
}
结果:
pool-1-thread-1执行了
pool-1-thread-3执行了
pool-1-thread-2执行了
pool-1-thread-2执行了
pool-1-thread-5执行了
main执行了
pool-1-thread-3执行了
pool-1-thread-4执行了
pool-1-thread-1执行了
由结果可以看出,由Main线程提交的任务,在队列满了的情况下,使用CallerRunsPolicy()拒绝策略的线程池,会再交给main线程去执行。
6.4DiscardOldestPolicy()
丢弃队列最前面的任务,然后重新提交被拒绝的任务(新任务入列)。