Java线程池的使用
前言
众所周知,近十年来摩尔定律逐渐在很多领域已经失效了,单个CPU
的性能已经很难满足科技的需求,所以现在很多时候会采用多核的方式来提升整个服务器的性能,那么如何重复发挥多核CPU
的性能,那么就不得不说使用多线程。
正文
关于线程池
为什么需要线程池?
线程是处理器调度的基本单位。我们会为每一个请求都独立创建一个线程,而操作系统创建线程、切换线程状态、结束线程都要使用CPU
进行调度。
Java线程池的使用
Java
当中主要有两类线程池:
-
Executor
线程池:Executor
是个简单的接口,它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable
来表示任务。Executor
基于"生产者-消费者"模式,提交任务的操作相当于生产者,执行任务的则相当于消费者。 -
ForkjoinPool
线程池:它非常适合执行可以分解子任务的任务,比如树的遍历,归并排序,或者其他一些递归场景。
Executor创建线程池的4种方式:
1.CachedThreadPool
- CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为
Integer.max_value
,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。(业务上允许run
执行失败)
CachedThreadPool
的默认构造函数
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool
的使用
public class Main15 {
public static ExecutorService mythread = Executors.newCachedThreadPool();
public static void main(String[] args) {
for (int i=0;i<10;i++){
mythread.execute(new Runnable() {
public void run() {
System.out.println("执行");
}
});
}
}
}
2.ScheduledThreadPool
- ScheduledThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
ScheduledThreadPool
的默认构造函数
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
ScheduledThreadPool
的使用
public class Main15 {
public static ExecutorService mythread = Executors.newScheduledThreadPool(1024);//核心线程数
public static void main(String[] args) {
for (int i=0;i<10;i++){
mythread.execute(new Runnable() {
public void run() {
System.out.println("执行");
}
});
}
}
}
3.SingleThreadPool
- SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
SingleThreadPool
的默认构造函数
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadPool
的使用
public class Main15 {
public static ExecutorService mythread = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
for (int i=0;i<10;i++){
mythread.execute(new Runnable() {
public void run() {
System.out.println("执行");
}
});
}
}
}
4.FixedThreadPool
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。(保证run
尽可能被处理)
FixedThreadPool
的默认构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
FixedThreadPool
的使用
public class Main15 {
public static ExecutorService mythread = Executors.newFixedThreadPool(1024);
public static void main(String[] args) {
for (int i=0;i<10;i++){
mythread.execute(new Runnable() {
public void run() {
System.out.println("执行");
}
});
}
}
}
特别要注意的是:
-
FixedThreadPool
能保证每一个Runnable
对象都不会丢失,线程过小会大量进入拒绝策略中,线程数过大,但是一定程度上可能会造成OOM
。在真实场景采用设置无限大,即Integer.max_value
。 - 选择合适的拒绝策略,能够一定程度上保证服务的高可靠。
Executor线程池的底层原理
线程池的逻辑结构
创建线程池的构造函数
public ThreadPoolExecutor(
int corePoolSize, #核心线程数
int maxinmumPoolSize, #线程总数 非核心数=总数-核心数
long keepAliveTime, #当前线程数大于核心线程数时 线程的等待新任务的等待时间(核心线程也会面临死亡)
TimeUnit unit, #时间单位
BlockingQueue<Runnable> workQueue #任务队列
RejectedExecutionHandler #(选填) 拒绝处理器
)
ThreadPoolExecutor
线程池处理线程的过程:
- 当前运行线程数 小于
corePoolSize
任务直接交给核心线程进行执行、 - 当前运行线程数 大于或等于
corePoolSize
任务且满足队列未满,那么 任务将进入任务队列进行等待,并且任务队列都具有阻塞性,所以只有当核心线程数的任务执行完了,才会从任务队列中获取任务。 - 当前运行线程数 大于或等于
corePoolSize
任务且队列已满,那么 任务进入非核心线程。 - 当核心线程、等待队列、非核心线程都被占用的时候线程会被拒绝器处理。
阻塞特性:当队列满了,便会阻塞等待,直到有元素出队,后续的元素才可以被加入队列。
任务队列
任务队列实现BlockingQueue
接口,即阻塞队列接口,具体有:
-
SyschronousQueue
:每一次add()
插入 必须要等待相对删除/读取操作 -
ArrayBlockingQueue
:数组的方式,大小创建后不能改变大小,具有阻塞特性。 -
LinkedBlockingQueue
:无限容量 基于链表的形式 -
LinkedBlockingDeque
:无限双端链表队列,可以从两头进行元素的读/取操作 -
PriorityBlockingQueue
:按照优先级进行内部元素排序的无限队列。 -
LinkedTransferQueue
:无限队列,先进先出,具有阻塞特性。
拒绝处理器
适用:那些既不能进入核心线程、等待队列,也无法使用非核心线程来处理,或者线程异常的线程:
-
CallerRunsPolicy
:直接运行该任务的run
方法,但不是在线程池内部,适合处理业务比较重要且数量不多的场景。 -
AbortPolicy
:RejectedExecutionException
异常抛出。适用对业务非常重要的完全不能不执行的场景。(默认) -
DiscardPolicy
:不会做任何处理。适合处理丢失对业务影响不大的场景。 -
DiscardOldestPolicy
:检查等待队列 强行取出队列头部任务(并抛弃该头部任务)后 再进行执行该任务。适合新数据比旧数据重要的场景。
特定场景如何去设置线程池
线程池的关键点是:
- 尽量减少线程切换和管理的开支;
- 最大化利用
CPU
。
对于1,要求线程数尽量少,这样可以减少线程切换和管理的开支;
对于2,要求尽量多的线程,以保证CPU
资源最大化的利用。
根据访问量场景给与以下的建议
- 高并发,低耗时的情况:建议少线程,只要满足并发即可;例如并发100,线程池可能设置为10就可以。
- 低并发,高耗时的情况:建议多线程,保证有空闲线程,接受新的任务;例如并发10,线程池可能就要设置为20;
- 高并发高耗时:1要分析任务类型,2增加排队,3、加大线程数
在springboot中的使用线程池
1.方式一:以注解的方式使用
@Configuration
@EnableAsync
public class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutro(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(10);//设置核心线程数
taskExecutor.setMaxPoolSize(50);//设置最大线程数
taskExecutor.setQueueCapacity(200);//线程池使用缓冲队列大小
taskExecutor.setKeepAliveSeconds(60);//最大存活时间
taskExecutor.setThreadNamePrefix("taskExecutor--");//设置线程名称的前缀
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);//当调度器shutdown被调用时等待当前被调度的任务完成
taskExecutor.setAwaitTerminationSeconds(60);//当调度器执行shutdown时,等待60秒后进行关闭
return taskExecutor;
}
/**
* 使用模板
* @param i
*/
@Async("taskExecutor")
public void tesTask(int i){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
2.以XML配置的方式来使用
<!-- 订单线程池 -->
<bean id="syncOrderExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 线程池维护线程的最少数量 -->
<property name="corePoolSize" value="1" />
<!-- 允许的空闲时间 -->
<property name="keepAliveSeconds" value="200" />
<!-- 线程池维护线程的最大数量 -->
<property name="maxPoolSize" value="5" />
<!-- 缓存队列 -->
<property name="queueCapacity" value="2000" />
<!-- 对拒绝task的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
源码
项目源码可从的我的github中获取:github源码地址