ThreadPoolExecutor创建线程池的配置详解

目录

上文代码及源码解析

线程池处理任务的优先级

BlockingQueue(线程池使用的缓冲队列)

缓冲队列详解

直接提交 SynchronousQueue

无界队列  LinkedBlockingQueue

有界队列  ArrayBlockingQueue

总结

RejectedExecutionHandler(拒绝策略)


在上篇文章《java线程的创建(重点:线程池的使用,线程池不允许使用Executors创建)》中提到了线程的创建方式和线程池的用法,阿里巴巴java规范不允许使用Executors创建线程池,那么可以使用ThreadPoolExecutor来手动创建线程池,下面是我对它的整理和理解

上文代码及源码解析

配置类ThreadPoolConfig

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;

@Configuration
public class ThreadPoolConfig {

    @Bean(value = "myThreadPool")
    public ExecutorService buildMyThreadPool(){
        // nameFormat- 一个String.format(String, Object...)兼容的格式字符串,将作为单个参数提供唯一的整数(0,1等)
        // 此整数对于ThreadFactory的构建实例是唯一的,并将按顺序分配。
        // 例如,"rpc-pool-%d"会产生像线程名称 "rpc-pool-0","rpc-pool-1","rpc-pool-2"
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-thread-%d").build();

        ExecutorService executorService = new ThreadPoolExecutor(
                18
                ,30
                ,0
                ,TimeUnit.MILLISECONDS
                ,new ArrayBlockingQueue<>(1000)
                ,threadFactory
                ,new ThreadPoolExecutor.CallerRunsPolicy()
        );
        return executorService;
    }
}

首先这个方法的返回值是java.util.concurrent.ExecutorService ,查看源码该接口继承Executor,Executor接口只有一个方法:

void execute(Runnable command);

回到此配置类代码,可以看出具体实现是调用java.util.concurrent.ThreadPoolExecutor,查看源码它继承抽象类AbstractExecutorService,而AbstractExecutorService实现ExecutorService接口,所以可以调用execute方法。

@Resource(name = "myThreadPool")
private ExecutorService myThreadPool;

//在某方法中执行...
executorService.execute(new Runnable() {
    @Override
    public void run() {
        //业务代码
    }
});

查看execute()方法的源码

//1. 获取当前线程池的状态
int c = ctl.get();
//2. 当前线程数量小于 coreSize 时创建一个新的线程运行
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
//3. 如果当前线程处于运行状态,并且写入阻塞队列成功
if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    //4. 双重检查,再次获取线程状态;
    //如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略
    if (! isRunning(recheck) && remove(command))
        reject(command);
    //5. 如果当前线程池为空就新创建一个线程并执行
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
//6. 如果在第3步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略
else if (!addWorker(command, false))
    reject(command);

再看ThreadPoolExecutor的配置,源码中的有参构造如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              RejectedExecutionHandler handler) {
    ......
}
corePoolSize:   线程池核心线程数量  int
maximumPoolSize:    线程池允许最大线程数量  int
keepAliveTime:  线程池中线程所允许的空闲时间   long
unit:   线程池维护线程所允许的空闲时间的单位

这四个参数上文已提到,其中线程数的确定请查看下一篇文章:《线程池线程数目的确定》

重点在BlockingQueueRejectedExecutionHandler

线程池处理任务的优先级

我对线程池处理任务的优先级理解为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

如corePoolSize=10,maximumPoolSize=15,当任务数大于corePoolSize,先往队列中放,队列满了,还有任务进来,才创建线程,但不会超过maximumPoolSize;此时,如果还有任务进来,已经达到maximumPoolSize,就会进行handler异常策略。

       我这样通俗说下来,会对理解下文有帮助。

BlockingQueue(线程池使用的缓冲队列)

提供三种策略

  • 直接提交。默认SynchronousQueue。queue将任务直接提交给线程而不保持它们。
  • 无界队列。将导致在所有corePoolSize 线程都忙时新任务在队列中等待。因此maximumPoolSize的值就无效了。一般使用LinkedBlockingQueue
  • 有界队列。当使用有限的maximumPoolSizes时,有界队列防止资源耗尽。一般使用ArrayBlockingQueue

缓冲队列详解

       首先要理解一点,如果运行的线程等于或多于 corePoolSize,则Executor始终首选将请求加入队列,而不添加新的线程;如果无法将请求加入队列,则创建新的线程。

  • 直接提交 SynchronousQueue

       该队列是将任务直接提交给线程而不保存它们。SynchronousQueue线程安全的Queue,可以存放若干任务(但只允许一个任务在等待),每个线程插入必须等待另一个线程移除,也就是说A任务进入队列,B任务必须等A任务被移除之后才能进入队列,否则执行异常策略。你来一个我扔一个,所以SynchronousQueue没有内部容量。如:

  1. 核心线程数为2,最大线程数为3;使用SynchronousQueue。
  2. 当前有2个核心线程在运行,又来了个A任务,2个核心线程忙碌,根据如果运行的线程等于或多于 corePoolSize,则Executor始终首选将请求加入队列,而不添加新的线程。所以A任务被添加到SynchronousQueue队列。
  3. 此时又来了个B任务,2个核心线程还没有执行完。新创建的线程正在执行A任务,所以B任务进入Queue后,最大线程数为3,没地方等待,只能执行异常策略。
  • 无界队列  LinkedBlockingQueue

       无界队列会使在所有核心线程都在忙时,新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值无意义,不会有新线程被创建,都在排队)。如果未指定容量,则它等于 Integer.MAX_VALUE。如果设置queue预定义容量,则当核心线程忙碌时,新任务会在队列中等待,超过预定义容量就会执行异常策略。你来一个我接一个,直到我容不下你了。FIFO,先进先出。如:

  1. 核心线程数为2,最大线程数为3;LinkedBlockingQueue(1),设置容量为1。
  2. 当前有2个核心线程在运行,又来了个A任务,2个核心线程忙碌,如果运行的线程等于或多于 corePoolSize,则Executor始终首选将请求加入队列,而不添加新的线程。所以A任务被添加到LinkedBlockingQueue队列。
  3. 此时又来了个B任务,2个核心线程没有执行完当前任务,A任务在队列中等待,队列已满。所以根据如果无法将请求加入队列,则创建新的线程
  4. B任务被新创建的线程所执行,此时又来个C任务,此时maximumPoolSize已满,队列已满,只能执行异常策略。
  • 有界队列  ArrayBlockingQueue

       操作模式跟LinkedBlockingQueue查不多,但必须为其设置容量。new ArrayBlockingQueue(Integer.MAX_VALUE) 跟 new LinkedBlockingQueue(Integer.MAX_VALUE)效果一样。LinkedBlockingQueue底层是链表结构,ArrayBlockingQueue底层是数组结构。你来一个我接一个,直到我容不下你了。FIFO,先进先出。

  • 总结

使用无界队列,要防止任务增长的速度远远超过处理任务的速度,控制不好可能造成内存溢出。

使用有界队列,关键在于调节线程数和Queue大小 ,线程数多,队列容量少,资源浪费;线程数少,队列容量多,性能低,也可能造成内存溢出。

RejectedExecutionHandler(拒绝策略)

表示任务(队列和最大线程数)已经饱和了,但还有线程进入,只好拒绝接受,并根据配置的不同策略进行不同的处理。

  • ThreadPoolExecutor.AbortPolicy()  抛出java.util.concurrent.RejectedExecutionException异常,并放弃任务
  • ThreadPoolExecutor.CallerRunsPolicy()  重试添加当前的任务,会自动重复调用execute()方法,能够减缓新任务的提交速度
  • ThreadPoolExecutor.DiscardOldestPolicy()  抛弃旧的任务(队列头部的任务先被删除)
  • ThreadPoolExecutor.DiscardPolicy()  抛弃当前的任务

你可能感兴趣的:(多线程)