1. 线程池简介
2. 线程池应用
3. 线程池参数详解
4. 如何正当设置线程池参数
1. 线程池简介
咱们先来介绍一下,什么是线程池?
在以往的博客中,咱们介绍线程的时候,无论是 Runnable,还是 Calllable 这些,最终都是应用 new Thread().start(); 的形式进行创立,其实在实在的生产环境中,咱们是禁止应用这种写法的,因为这样有几个重大的弊病:
a. 每次 new Thread() 新建对象,会造成开销,导致服务器运行性能变差。
b. 在无限度 new Thread()的状况下,咱们一不小心就会创立过多的线程,相互之间竞争资源,可能导致上下文频繁切换,占用资源过多,死锁或者 oom。
c. 不足更多功能,如定时执行、定期执行、线程中断。
于是,JAVA 线程池就产生了!
JAVA 线程池做的次要工作是 管制运行的线程的数量 ,解决过程中将工作放入队列而后在线程创立后启动这些工作,如果线程数量超过了最大执行数量, 超出数量的线程将排队等待,期待其它线程执行结束,再从队列中取出工作来执行。
它的次要特点:线程服用,管制最大并发数,治理线程。
第一:升高资源耗费 ,通过反复利用本人创立的线程升高线程创立和销毁造成的耗费。
第二:进步响应速度 ,当工作达到时,工作能够不须要等到线程创立就能立刻执行
第三:进步线程的可管理性,线程是稀缺资源,如果无限度地创立,不仅会耗费系统资源,还会升高零碎的稳定性,应用线程池能够对立地进行调配,调优和监控。
多说无用,咱们来看看如何应用 JAVA 线程池吧!
2. 线程池应用
咱们先介绍一下 JAVA 自带的创立线程池的工具类!
Executors
它提供了三个办法来创立线程池:
ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池固定数线程
ExecutorService threadPoo2 = Executors.newCachedThreadPool();// 一池多线程
ExecutorService threadPoo3 = Executors.newSingleThreadExecutor();// 一池一线程
顾名思义:
newFixedThreadPool()办法就是创立一个 固定线程数 的线程池,
newCachedThreadPool()办法会依据你计算机和工作的状况,进行主动增减线程
newSingleThreadExecutor() 创立一个单线程的线程池,每次只能执行一个工作
同时创立完线程池之后,应用线程池自带的submit 或者 excute 办法,而后用 lambda 表达式传入函数体后,就能够执行工作了!
threadPool.submit();
threadPool.execute();
这时,咱们能够应用一个线程池尝试着应用一下
ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池固定数线程
// 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
try {for (int i = 1; i < 10000; i++) {threadPool.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {e.printStackTrace();
} finally {threadPool.shutdown();
}
咱们创立了一个线程池,固定数量为 5,而后启动 10000 个线程,看看运行状况。
能够看出,线程编号始终没有超过咱们设置的初始值 5。
这时,咱们能够换一个线程池应用一下,咱们应用 cache 线程池:
ExecutorService threadPoo1 = Executors.newCachedThreadPool();// 一池多线程
// 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
try {for (int i = 1; i < 10000; i++) {threadPoo1.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {e.printStackTrace();
} finally {threadPoo1.shutdown();
}
能够看出,线程启动地越多,该线程池就会启动越多的线程来适应需要。
让咱们来看一下最初一个线程池:
ExecutorService threadPoo1 = Executors.newSingleThreadExecutor();// 一池一线程
// 模仿 10000 个用户来办理业务,每一个用户就是来自内部的申请线程
try {for (int i = 1; i < 10000; i++) {threadPoo1.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {e.printStackTrace();
} finally {threadPoo1.shutdown();
}
查看后果:
能够看出,永远 只有一个线程 在运行。
是不是十分神奇?!咱们能够看一下创立线程池的办法到底怎么执行地,竟然有这种成果!
首先看一下固定数线程的办法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, // 常驻外围线程数
nThreads,// 最大线程数
0L,// 多余的闲暇线程的存活工夫
TimeUnit.MILLISECONDS,// 多余线程存活单位
new LinkedBlockingQueue<Runnable>()); // 应用的阻塞队列
}
cache 线程池的办法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
单线程池的办法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从我给的源码正文中能够看出,
固定数线程池最大和常驻线程数都是传入的参数
而 cache 线程池的最大线程数是 Integer.MAX_VALUE
单线程池底层应用了 SynchronousQueue : 不存储元素的阻塞队列,即单个元素的队列。
多线程池的底层应用 了 LinkedBlockingQueue : 由链表组成的有界(然而默认值为 Integer.MAX_VALUE 阻塞队列 )
阻塞队列能够看我之前写的这篇文章:JAVA 并发编程——阻塞队列实践及其 API 介绍
所以就导致了为什么会有以上的运行后果!
所以在生产中,咱们到底如何应用呢??
答案是————一个都不应用 ,因为 LinkedBlockingQueue 它的最大长度是 Integer.MAX_VALUE, 那就相当于无界 , 可能会让服务器内存爆仓产生 OOM,所以接下来,咱们学习一下自定义创立线程池的参数!
3. 线程池参数详解
咱们先点到 ThreadPoolExecutor 这个类里看一下,它到底有几个参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
咱们能够看到,有七个参数
corePoolSize 线程池中 常驻外围线程数
maximumPoolSize 线程池可能包容的同时执行的 最大线程数 ,此值必须大于等于 1
keppAliveTime 多余 的闲暇线程的 存活工夫
unit 存活工夫 单位
workQueue 工作队列,被提交然而尚未执行的工作
threadFactory 示意生成线程池中工作线程的线程工厂,用于创立线程,个别默认即可
handler 回绝策略,示意 队列满了并且工作线程大于等于线程池的最大线程数,如何解决。
后面几个参数都好了解,重要的是这个办法能够自定义阻塞队列的类型,咱们就能够防止应用 LinkedBlockingQueue 了。
接下来,咱们看一下 handler 回绝策略:
当外围线程数满了,阻塞队列也满了的时候,再进来的线程,就会启动回绝策略,个别常见的回绝策略有这几种:
AbortPolicy(默认):间接抛出 RejectExcutionException 异样阻止线程运行。
CallerRunsPolicy: 将多进去的工作 退还给调用者,从而升高流量。
DiscardOldestPolicy:摈弃期待队列中期待最久的工作,而后把当前任务退出期待队列。
DiscardPolicy:间接抛弃工作。
接下来,咱们尝试一下本人新建线程池,并尝试着应用该线程池执行一下工作。
ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(2,// 常驻外围线程数
5,// 最大外围线程数
1L,// 多余的闲暇线程的存活工夫
TimeUnit.SECONDS,// 多余闲暇线程存活工夫单位
new ArrayBlockingQueue<Runnable>(10),// 阻塞队列
Executors.defaultThreadFactory(),// 生成线程池中工作线程的线程工厂
new ThreadPoolExecutor.AbortPolicy());// 回绝策略
// 模仿多个用户来办理业务,每一个用户就是来自内部的申请线程
try {for (int i = 1; i < 10000; i++) {threadPoolExecutor.submit(() -> {System.out.println(Thread.currentThread().getName() + "正在办理业务");
});
}
} catch (Exception e) {e.printStackTrace();
} finally {threadPoolExecutor.shutdown();
}
}
在应用以后回绝策略下,很显著能够看出,执行线程超出线程池的数量了,会抛异样,咱们来看一下运行后果:
咱们能够换一个回绝策略试一下:比如说 DiscardPolicy(摈弃工作)
ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(2,// 常驻外围线程数
5,// 最大外围线程数
1L,// 多余的闲暇线程的存活工夫
TimeUnit.SECONDS,// 多余闲暇线程存活工夫单位
new ArrayBlockingQueue<Runnable>(10),// 阻塞队列
Executors.defaultThreadFactory(),// 生成线程池中工作线程的线程工厂
new ThreadPoolExecutor.DiscardPolicy());// 回绝策略
它就运行实现了,且没有抛出任何异样。
4. 如何正当设置线程池参数
如何配置线程池的参数,其实咱们最须要配置的参数只有一个:
corePoolSize 线程池中常驻外围线程数
当咱们的线程池应用场景是在 CPU 密集型(过程绝大部份工作依附 cpu 的计算能力实现),那么为了避免上下文的切换造成的开销,咱们个别设置 最大线程数等于 CPU 核数(核数获取办法:Runtime.getRuntime().availableProcessors();)
当咱们的线程池应用场景是在 IO 密集型(绝大部分工作就是在读入,输入数据), 那么 cpu 不是十分十分忙碌,大部分在期待的状况下, 最大线程能够是 CPU 核数的两倍。
5. 总结
这次咱们介绍了线程池的原理以及参数详解,最重要的是,创立线程池必须要手动应用构造方法创立,不能应用默认的办法类进行创立!
最初,咱们用两张图来概括一下线程池的执行流程:
好了,明天的分享就到这里!