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线程池的底层原理

线程池的逻辑结构

Java线程池的使用_任务队列

创建线程池的构造函数

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​​:检查等待队列 强行取出队列头部任务(并抛弃该头部任务)后 再进行执行该任务。适合新数据比旧数据重要的场景。

特定场景如何去设置线程池
线程池的关键点是:

  1. 尽量减少线程切换和管理的开支;
  2. 最大化利用​​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源码地址​

Java线程池的使用_java_02