JUC_线程池详解与线程池调优

学习线程池的时候,要对线程有一定理解

池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

池化(pool)技术的本质是通过复用对象、连接等资源,减少创建对象/连接,降低垃圾回收(GC)的开销,适当使用池化相关技术能够显著提高系统效率,优化性能。

程序的运行,本质:占用系统的资源! 优化资源的使用!

线程的创建、销毁。十分浪费资源

池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

我对线程池的理解:

众所周知,我们在创建线程的时候,需要new一个Thread的对象,这样每次都创建一个对象,不仅会消耗不必要的资源,而且如果并发量特别高的话,线程就会创建得特别的多,会造成OOM(内存溢出)的问题

那么线程池可以解决这些问题,假设(我这里为了方便初学的同学,可以先这样理解着),我们创建一个有10个线程的线程池放在那,外部请求来了,会调用线程池内部的10个线程,每个线程执行完自己的任务,会跑回线程池待着,等待下一个任务的调用,省去了创建线程的时间,并且如果并发量高了的话,我们会禁止其他请求的访问,就不会一直重新开线程,造成OOM的问题了

线程池的优势:线程复用、可以控制最大并发数、管理线程

总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池讲解:三大方法、七大参数、四种拒绝策略

七大参数:

创建线程池,最核心的方法就是ThreadPoolExecutor这个类的这个构造方法,这个方法有7个参数是特别重要的。

    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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

讲解:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

我的图解:核心线程数为4,最大线程数为8,队列为5

1、刚开始创建的时候:

在这里插入图片描述

2、当任务小于核心线程数的时候:来了四个任务
这四个任务先进任务队列,核心线程有空着的,就拿任务来用

在这里插入图片描述

3、当任务小于任务队列+核心线程数的时候:来了八个任务

在这里插入图片描述

4、当任务大于任务队列+核心线程数的时候:来了11个任务
(1)、先进任务队列,结果任务队列不让进,就去尝试最大线程数

在这里插入图片描述

(2)、最大线程数没满,线程池就开辟新的线程(使用线程工厂),他们就插队先执行

在这里插入图片描述

5、当任务大于最大线程数+任务队列的时候:来了15个任务
这里也是先进任务队列,结果进不了,再看最大线程数,结果最大线程数也满了,就要用到拒绝策略了

在这里插入图片描述

6、当没任务了:0个任务

在这里插入图片描述

7、过了KeepAliveTime后,线程池会回到原点

在这里插入图片描述

三大方法:(工作不能用,但必须了解)(阿里开发手册中强制规定不能用)(为啥不能用,因为在高并发的情况下,你的服务器会因为这个线程池而“爆掉”)

这三大方法是基于java.util.concurrent包(JUC)下面的Executors这个工具类来调用的

在这里插入图片描述

1、ExecutorService threadPool = Executors.newSingleThreadExecutor(); 单线程


public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        //根据刚才我讲解的不难看出,这里只有一个线程,没有拒绝策略,而是用任务列表,一直加
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
在高并发的情况下,会造成的问题,他一直往队列中加任务,会造成OOM(内存溢出),简单来说就是你内存不够了,直接卡死

2、ExecutorService threadPool = Executors.newFixedThreadPool(5);固定线程个数


public static ExecutorService newFixedThreadPool(int nThreads) {
    // 这里我们是自己定义了一个核心线程数,与最大线程数,不难看出非核心线程数(最大线程数-核心线程数)根本就没有,如果超出了,没有拒绝策略,也是用任务队列一直加
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
在高并发的情况下,会造成的问题,他一直往队列中加任务,会造成OOM(内存溢出),简单来说就是你内存不够了,直接卡死

3、ExecutorService threadPool = Executors.newCachedThreadPool();缓存池,可扩展

public static ExecutorService newCachedThreadPool() {
    // 这里没有核心线程数,但有大量的非核心线程数(最大线程数-核心线程数)
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
高并发的情况下,会造成CPU效率100%,从而烧掉CPU

四种拒绝策略(对应四个实现类)

在这里插入图片描述

  • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行
  • CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
  • DiscardPolicy - 直接丢弃,其他啥都没有,不会抛异常
  • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

我的理解(白话文):

  • AbortPolicy :丢掉任务,并抛异常
  • CallerRunsPolicy :任务将由调用者线程去执行。就是说你如果是主函数调用的,多的任务会由主函数执行
  • DiscardPolicy:丢掉任务,不抛异常
  • DiscardOldestPolicy :抛弃进入任务队列最早的那个任务,然后尝试把这次的任务放入任务队列

线程池参数调优:没有最优的,要按实际情况改,最后附我最常有的参数

既然不能用Executors去设定线程池,那么我们就要自己去设置线程池的大小,怎样才能得到最优的值呢?

一、了解:IO密集型,CPU密集型

CPU密集型(CPU bound)

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),

I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。

例如,一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

IO密集型(I/O bound)

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

CPU密集型 vs IO密集型

我们可以把任务分为计算密集型和IO密集型。

CPU密集型

一般来说:计算型代码、Bitmap转换、Gson转换等

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。

这种计算密集型任务虽然也可以用多任务完成,但是,任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,

所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。

Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

IO密集型

一般来说:文件读写、DB读写、网络请求等

第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,

任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。

对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。

对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

总之,计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。

调优参数:

  1. CPU密集型任务

    尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

  2. IO密集型任务

    可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

  3. 混合型任务

    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

获取CPU的核数

// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());

我的线程池(单例模式,让这个线程池全局唯一,当然你用反射破解了我也无话可说):

/**
 *  线程池工具类,单例模式,全局唯一
 * @author ZHAOPINGAN
 */
public class ThreadPoolUtil {

    private static volatile ExecutorService threadPool = null;

    private ThreadPoolUtil(){}

    public static ExecutorService getThreadPool() {
        if (threadPool == null) {
            synchronized (ThreadPoolUtil.class) {
                if (threadPool == null) {
                    threadPool = new ThreadPoolExecutor(
                            // 核心线程数
                            4,
                            // 最大线程数
                            10,
                            // 10s 过期时间
                            10000L, TimeUnit.MILLISECONDS,
                            // 队列 1000
                            new LinkedBlockingQueue<>(1000),
                            // 线程池工厂
                            Executors.defaultThreadFactory(),
                            // 拒绝策略 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息
                            new ThreadPoolExecutor.AbortPolicy()
                    );
                }
            }
        }
        return threadPool;
    }
}

借鉴:

星夜孤帆: CPU密集型与IO密集型

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我认不到你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值