多线程(1)------ThreadPoolExecutor底层原理

多线程(1)------线程池

前言

池化技术在我们的项目中是使用很频繁的,除了之前数据库的连接池,还有处理任务的线程池,在本文中将会来研究线程池的使用和原理.

正文

人类认识事物的方式有以下几步:是什么,能干什么,怎么用,原理.

所以本文将通过以上几步来逐步学习.

1. 线程池是什么?

在以前的单线程项目中,我们执行业务逻辑的流程是串行化的,一个main方法,然后依次调用其他的方法,这整个执行的流程是有序的.

在后来,我们发现使用Thread类可以启动多线程来帮助我们处理不同的任务,比如三个任务A,B,C,他们的调用关系是A->B->C,其中A和C耗时1s,C任务比较复杂,耗时2秒.那么此时如果我们用单线程的话,总共耗时需要4s.

如果我们使用Thread的方式来创建多线程去单独运行B任务的话,这样就会在2s内完成整个任务流程.

以上的方式相信大家都能理解.

现在问题来了,如果我们的任务很多,每次调用都要去new Thread来启动一个线程,那么随着任务数量的增多,我们的Thread会越来越多,会严重影响服务器的性能.在这种情况下,池化技术为我我们实现了线程池来帮助我们维护众多的线程.

所谓线程池,就是在一个池中维护很多的线程,当我们要使用的时候就取出一个线程,用完以后不销毁,放回池中,这样就可以保证线程的复用,降低系统因为创建和销毁线程带来的资源损耗.

线程池的工作主要是控制运行的线程数量,处理过程中将任务放入阻塞队列,然后在线程创建后启动这些任务.

特点:

  1. 线程复用
  2. 管理线程,控制最大并发数
2.线程池的使用

在java中,线程池就是Executor,但是在使用的时候,我们并不是直接来用这个接口,而是通过一个工具类:Executors来创建线程池.

通过Executors创建的线程池有很多种,在这里我们需要知道以下三种:

  • Executors.newFixedThreadPool()

    创建一个固定线程数量的线程池

  • Executors.newSingleThreadPool()

    创建一个只有一个线程的线程池

  • Executors.newCachedThreadPool()

    创建一个线程数量可变的线程池

我们可以写简单的代码来熟悉线程池的使用:

       Executor executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 运行...");
            });
        }

以上代码中,我们创建了5个线程的线程池,提交10个打印任务到线程池,下面是打印结果:

pool-1-thread-2 运行…
pool-1-thread-3 运行…
pool-1-thread-1 运行…
pool-1-thread-4 运行…
pool-1-thread-4 运行…
pool-1-thread-4 运行…
pool-1-thread-3 运行…
pool-1-thread-2 运行…
pool-1-thread-5 运行…
pool-1-thread-1 运行…

可以看到,10个任务都是被5个线程轮询完成的.

虽然JDK为我们提供了Executors类来创建线程池,但是不建议使用,尤其在阿里巴巴java开发手册中明确说明,不应该使用Executor来创建线程池,应该使用ThreadPoolExecutor来创建线程池.

那么ThreadPoolExecutor是什么呢?我么一起来看一下

3.线程池的原理

我们知道,创建线程池都可以用Executors来new不同类型,我们看一下源码会发现其实他底层用的都是一个类来实现的:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

在这三种创建线程池的方法中,底层都是使用ThreadPoolExecutor来创建的,在它的构造方法中,有很多的参数,我们来看看这些参数的意思.

1. ThreadPoolExecutor中的七大参数

在ThreadPoolExecutor的构造中,一共有七个参数需要设置,源码如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面是每个参数的意思:

  1. corePoolSize: 线程池常驻核心线程数量
  2. maximumPoolSize: 线程池能够容纳的最大线程数量
  3. workQueue: 任务队列,用于缓存提交后没有执行的任务
  4. keepAliveTime: 多余空闲线程的存活时间.当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余的空闲线程会被销毁,只剩下corePoolSize.
  5. unit: keepAliveTime的单位.
  6. threadFactory: 生产线程池中工作线程的线程工厂,用于创建线程,一般使用默认的
  7. handler: 饱和拒绝策略,表示当任务队列满了并且工作线程大于等于线程池的最大线程数是如何来安排新任务.

根据以上的参数配置,ThreadPoolExecutor会为我们创建一个线程池,该线程池底层的工作原理如下:

  • 当我们调用调用了exeute()方法以后,线程会做如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
    • 如果正在运行的线程数量大于或者等于corePoolSize,那么将这个任务放入阻塞队列
    • 如果这个时候任务队列满了,且正在运行的线程数量还小于maximumPoolSize,那么就创建非核心线程运行这个任务
    • 如果队列满了且正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当一个线程无事可做超过keepAliveTime时间时,线程池会判断:
    • 如果当前运行的线程数大于corePoolSize,那么这个线程会被销毁,当所有线程池的任务完成后,线程池会收缩到corePoolSize的大小.

具体的流程图如下:

在这里插入图片描述

2. 拒绝策略

具体的饱和拒绝策略有一下四中:

  1. AbortPolicy: 这是默认使用的策略,它湖直接抛出RejectedExecutionExeception异常阻止系统正常运行.
  2. CallerRunsPolicy: ""调用者运行,即不会抛弃任务或者抛出异常,而是让调用它的那个线程去执行.
  3. DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中,并尝试再次提交当前任务.
  4. DiscardPolicy: 直接丢弃任务,不会处理,也不会抛出异常.
3. 实际使用线程池时的参数配置

其实虽然JDK为我们实现了很多的线程池,但是在实际的使用中我们还是会自定义的来使用.那么在实际生产环境下,对于maximumPoolSize我们是如何配置的呢?

一般来说,会按照以下的方式配置最大线程数量:

CPU密集型任务

如果该任务需要大量的运算,而没有阻塞,那么这个时候应该配置尽可能少的线程数量.如代码在while循环中运算

数量: CPU核数+1 个线程的线程池.

IO密集型任务*

因为IO密集型任务并不是一直在执行任务,则应该配置尽可能多的线程,如CPU核数*2

此类任务会大量阻塞,因此需要更多的线程数.

数量: CPU核数/1-阻塞系数

阻塞系数在0.8~0.9之间

比如8核CPU: 8/1-0.9 = 80个线程数.

总结

在本篇文章中,对JDK的ThreadPoolExecutor的使用做了介绍,在实际的工作中我们应该根据服务器以及业务的具体情况来使用线程池.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值