做Java开发这些年,线程池的使用细节和坑你真的了解吗?仔细看看吧~

目录

一、【阿里巴巴编码规范】 线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因

 二、ThreadPoolExecutor构造函数里面的参数以及使用

三、execute源码分析

高并发下核心线程数配置:

四、自定义线程池以及使用

 五、线程池的几种执行方法都有哪些区别?

六、什么是线程的拒绝策略

七、如何自定义拒绝策略?

八、ThreadPoolExecutor 能不能实现扩展?如何实现扩展?



一、【阿里巴巴编码规范】 线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因

Executors创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等,如果使用不当,会造成资源耗尽问题;
直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险

常见的线程池问题:
newFixedThreadPool和newSingleThreadExecutor: 
    队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM

newScheduledThreadPool和newCachedThreadPool:
    线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM 

 

 二、ThreadPoolExecutor构造函数里面的参数以及使用

看这些参数,再看第三步execute的源码,会对线程池的执行有个深刻的印象~

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:
线程池创建的核心线程数,线程池维护线程的最少数量,如果设置为0,则表示在没有任何任务时候,销毁线程池。如果大于0,即使没有任务需要执行核心线程会一直存活
需要注意的是,此值如果设置的比较小,则会频繁的创建和销毁线程,如果设置的比较大,则会浪费系统资源,需要根据实际业务来调整此值
如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
坑:在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到corePoolSize
​
maximumPoolSize:
线程池维护线程的最大数量,超过将被阻塞
最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
任务队列已满时, 且当线程数=maxPoolSize,,线程池会拒绝处理任务而抛出异常
坑:当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程
​
keepAliveTime:
keepAliveTime表示线程的存活时间,当线程池空闲时并且超过此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于corePoolSize为止,
如果maximumPoolSize 等于corePoolSize,那么线程池在空闲的时候也不会销毁任何线程

unit:指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS
​
workQueue:
表示线程池执行的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行
​
threadFactory:
创建新线程时使用的工厂,此参数一般用的比较少,通常会使用默认的线程创建工厂的方法来创建线程
​
handler:
 RejectedExecutionHandler表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列workQueue中存储满了之后,
 并且不能创建新的线程来执行此任务时,就会用到此拒绝策略 
 // rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
// AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
// DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
// DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列

三、execute源码分析

看看execute里面的执行步骤,然后再总结一下加深记忆呀

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
      
        int c = ctl.get();
		  // 当前工作的线程数小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
		  // 创建新的线程执行此任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
		  // 检查线程池是否处于运行状态,如果是则把任务添加到队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
		  // 再次检查线程池是否处于运行状态,防止在第一次校验通过后线程池关闭
        // 如果是非运行状态,则将刚加入队列的任务移除
            if (! isRunning(recheck) && remove(command))
                reject(command);
		 // 如果线程池的线程数为 0 时(当 corePoolSize 设置为 0 时会发生)
            else if (workerCountOf(recheck) == 0)
			// 新建线程执行任务
                addWorker(null, false);
        }
		   // 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
        else if (!addWorker(command, false))
		//其中 addWorker(Runnable firstTask, boolean core) 方法的参数说明如下:
		//firstTask,线程应首先运行的任务,如果没有则可以设置为 null;
		//core,判断是否可以创建线程的阀值(最大值),如果等于 true 则表示使用 corePoolSize 作为阀值,false 则表示使用 maximumPoolSize 作为阀值。
		   // 执行拒绝策略
            reject(command);
    }

总结一下加深记忆:

查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第二步。
查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执行第三步。
查看线程池是否已满,即是否达到最大线程池数,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。

高并发下核心线程数配置:

如果是CPU密集型任务,那么线程池的线程个数应该尽量少一些,一般为CPU的个数+1条线程。

如果是IO密集型任务,那么线程池的线程可以放的很大,如2*CPU的个数。

对于混合型任务,如果可以拆分的话,通过拆分成CPU密集型和IO密集型两种来提高执行效率;如果不能拆分的的话就可以根据实际情况来调整线程池中线程的个数。

四、自定义线程池以及使用

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {


    @Bean("threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor(){


        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
        //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
        threadPoolTaskExecutor.setCorePoolSize(4);


      //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
      //当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
        threadPoolTaskExecutor.setMaxPoolSize(8);


      //缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
        threadPoolTaskExecutor.setQueueCapacity(124);
      
      //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        //允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
        //如果allowCoreThreadTimeout=true,则会直到线程数量=0
        threadPoolTaskExecutor.setKeepAliveSeconds(30);
        
        //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。 
        //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
        threadPoolTaskExecutor.setThreadNamePrefix("小滴课堂Spring自带Async前缀:");
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
        
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }


}

核心线程数根据业务来调试决定。 

 五、线程池的几种执行方法都有哪些区别?

execute() VS submit()
execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
来看两个方法的具体使用:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10L,
        TimeUnit.SECONDS, new LinkedBlockingQueue(20));
// execute 使用
executor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, execute.");
    }
});
// submit 使用
Future future = executor.submit(new Callable() {
    @Override
    public String call() throws Exception {
        System.out.println("Hello, submit.");
        return "Success";
    }
});
System.out.println(future.get());
以上程序执行结果如下:
Hello, submit.
Hello, execute.
Success

从以上结果可以看出 submit() 方法可以配合 Futrue 来接收线程执行的返回值。它们的另一个区别是 execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法


六、什么是线程的拒绝策略

当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。
自带的拒绝策略有 4 种:

AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;
CallerRunsPolicy,把任务交给当前线程来执行;
DiscardPolicy,忽略此任务(最新的任务);
DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。

AbortPolicy 的拒绝策略,代码如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new ThreadPoolExecutor.AbortPolicy()); // 添加 AbortPolicy 拒绝策略
for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}
以上程序的执行结果:
复制代码
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.wnn.ThreadPoolExample$$Lambda$1/1096979270@448139f0 rejected from java.util.concurrent.ThreadPoolExecutor@7cca494b[Running, pool size = 3, active threads = 3, queued tasks = 2, 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 com.wnn.ThreadPoolExample.rejected(ThreadPoolExample.java:35)
 at com.wnn.ThreadPoolExample.main(ThreadPoolExample.java:26)

 可以看出当第 6 个任务来的时候,线程池则执行了 AbortPolicy  拒绝策略,抛出了异常。因为队列最多存储 2 个任务,最大可以创建 3 个线程来执行任务(2+3=5),所以当第 6 个任务来的时候,此线程池就“忙”不过来了。


七、如何自定义拒绝策略?

自定义拒绝策略只需要新建一个 RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可,如下代码所示:
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new RejectedExecutionHandler() {  // 添加自定义拒绝策略
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                // 业务处理方法
                System.out.println("执行自定义拒绝策略");
            }
        });
for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}
执行结果:
执行自定义拒绝策略
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2
可以看出线程池执行了自定义的拒绝策略,我们可以在 rejectedExecution 中添加自己业务处理的代码。

八、ThreadPoolExecutor 能不能实现扩展?如何实现扩展?

ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间
public class ThreadPoolExtend {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程池扩展调用
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(2, 4, 10,
                TimeUnit.SECONDS, new LinkedBlockingQueue());
        for (int i = 0; i < 3; i++) {
            executor.execute(() -> {
                Thread.currentThread().getName();
            });
        }
    }
   /**
     * 线程池扩展
     */
    static class MyThreadPoolExecutor extends ThreadPoolExecutor {
        // 保存线程执行开始时间
        private final ThreadLocal localTime = new ThreadLocal<>();
        public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                            TimeUnit unit, BlockingQueue workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
        /**
         * 开始执行之前
         * @param t 线程
         * @param r 任务
         */
        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            Long sTime = System.nanoTime(); // 开始时间 (单位:纳秒)
            localTime.set(sTime);
            System.out.println(String.format("%s | before | time=%s",
                    t.getName(), sTime));
            super.beforeExecute(t, r);
        }
        /**
         * 执行完成之后
         * @param r 任务
         * @param t 抛出的异常
         */
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            Long eTime = System.nanoTime(); // 结束时间 (单位:纳秒)
            Long totalTime = eTime - localTime.get(); // 执行总时间
            System.out.println(String.format("%s | after | time=%s | 耗时:%s 毫秒",
                    Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));
            super.afterExecute(r, t);
        }
    }
}
以上程序的执行结果如下所示:
复制代码
pool-1-thread-1 | before | time=4570298843700
pool-1-thread-2 | before | time=4570298840000
pool-1-thread-1 | after | time=4570327059500 | 耗时:28.2158 毫秒
pool-1-thread-2 | after | time=4570327138100 | 耗时:28.2981 毫秒
pool-1-thread-1 | before | time=4570328467800
pool-1-thread-1 | after | time=4570328636800 | 耗时:0.169 毫秒

总结一下:线程池的使用必须要通过 ThreadPoolExecutor 的方式来创建,这样才可以更加明确线程池的运行规则,规避资源耗尽的风险。同时,也介绍了 ThreadPoolExecutor 的七大核心参数,
包括核心线程数和最大线程数之间的区别,当线程池的任务队列没有可用空间且线程池的线程数量已经达到了最大线程数时,则会执行拒绝策略,Java 自动的拒绝策略有 4 种,用户也可以通过重写 
rejectedExecution() 来自定义拒绝策略,我们还可以通过重写 beforeExecute() 和 afterExecute() 来实现 ThreadPoolExecutor 的扩展功能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值