线程池的理解与使用

为什么要使用线程池?

线程是一个稀缺资源,每创建一个线程都需要消耗系统的资源,如果不对创建线程做做控制,系统的资源就会被耗尽,最终导致服务器卡顿或宕机。所以要对线程的创建做一个控制,达到一定的数量后就不能在创建了。所以线程池就出来了,由线程池来统一的管理线程。

在线程池中维护一个稳定的线程数量,有任务需要执行就到线程池中申请一个线程去执行,可以达到资源的控制。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务如果线程的数量超过最大数量,超过数量的线程将排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

使用线程池的好处

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优,来保证系统的稳定运行。

jdk提供的默认线程池:

单线程exrcutor.newSinglethreadExecutor

固定长度线程池executors.newFixedThreadPool

动态线程池executor.newC爱车的Threadpool

定时任务线程executors.newScheduleadThreadPool

在阿里的规范中,明确的规定不能使用这些线程池,原因如下:

默认的线程池问题,在于任务队列通常用linkedblockqueue,这种队列的底层都是基于联表的,没有长度限制,如果当前任务项目中,任务量巨大的时候,线程池处理不过来的情况下,也不会触发拒绝策略,而是持续的往任务队列(阻塞队列)中添加任务,当达到一定的任务量之后,会导致内存大占用比较大,可能导致oom 内存溢出

线程池基本使用

 //获取当前机器的核数    
private static Integer code = Runtime.getRuntime().availableProcessors();

static {
        threadPoolExecutor = new ThreadPoolExecutor(
                code, // 核心线程数(线程池中始稳定的线程最小数量)
                code * 2, // 最大线程数量
                3, // 空闲线程时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(500), // 队列
                Executors.defaultThreadFactory(), // 线程创建工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );
    }

线程池7大核心参数 

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。如果线程池中的线程少于此数目,则在执行任务时创建。
  • maximumPoolSize:池允许最大的线程数,当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程。
  • keepAliveTime:超过corePoolSize之后的“临时线程”的存活时间。
  • unit:keepAliveTime的单位。
  • workQueue:当前线程数超过corePoolSize时,新的任务会处在等待状态,并存在workQueue中,jdk中提供了四种工作队列。
  • threadFactory:创建线程的工厂类,通常我们会自顶一个threadFactory设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位。
  • handler:线程池执行拒绝策略,当线数量达到maximumPoolSize大小,并且workQueue也已经塞满了任务的情况下,线程池会调用handler拒绝策略来处理请求。

拒绝策略

AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。

DiscardPolicy:直接抛弃不处理。

DiscardOldestPolicy:丢弃队列中最老的任务。

CallerRunsPolicy:将任务分配给当前调用execute方法线程来处理(同步执行)

线程池工作队列

1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。ArrayBlockingQueue在生产者放入数据费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue。

2、LinkedBlockingQueue
一个基于链表结构的有界阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue,在默认构造方法中容量是Integer.MAX_VALUE。

3、PriorityBlockingQueue,有优先级的无界阻塞队列,优先级通过参数Comparator实现。通过构造方法参数设置容量和比较器,initialCapacity和comparator分别是队列的初始容量和队列中对象的比较器。初始容量默认为11。比较器默认为null。put方法中有扩容,添加元素,比较。

4、SynchronousQueue:一个没有容量的队列,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程,这种线程池设置的最大线程数量是接近无限大的,CachedThreadPool就是这样的。

队列相关API

LinkedBlockingQueue和ArrayBlockingQueue区别

1、队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。

2、数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。

3、由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。

4、两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

线程池核心数的设置

线程数的设置的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。

1、当线程池的核心线程数量过大或者过小的影响

当线程池中核心线程数量过大时,线程与线程之间会争取CPU资源,这样就会导致上下文切换。过多的上下文切换会增加线程的执行时间,影响了整体执行的效率。

当线程池中的核心线程数量过少时,如果同一时间有大量任务需要处理,可能会导致大量任务在队列中排队等待执行,甚至会出现队列满了之后任务无法执行的情况,或者大量任务堆积在任务队列导致内存溢(OOM)。

2、核心线程数设置为多少才是最合适的?

核心线程数的设置需要分析线程池处理的程序是CPU密集型,还是IO密集型。

CPU密集型

CPU密集型也可以叫做计算密集型,指的是CPU有许多操作计算要进行处理,这时候CPU加载很高。比如一个计算程序要进行大量计算,这时候大部分时间都处在计算的过程中,CPU占用率就很高。

比如说要计算1+2+3+…+ 1亿、计算圆周率后几十位或者数据分析。都是属于CPU密集型程序。此类程序运行的过程中,CPU占用率一般都很高。

假如在单核CPU情况下,线程池有6个线程,但是由于是单核CPU,所以同一时间只能运行一个线程,考虑到线程之间还有上下文切换的时间消耗,还不如单个线程执行高效。所以!!!单核CPU处理CPU密集型程序,就不要使用多线程了。

假如是6个核心的CPU,理论上运行速度可以提升6倍。每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。所以!!!多核CPU处理CPU密集型程序才合适,而且中间可能没有线程的上下文切换(一个核心处理一个线程)。

简单的说,CPU密集型就是需要CPU疯狂的计算。比如,计算型代码、Bitmap转换、Gson转换等

IO密集型

IO密集型跟CPU密集型正好相反,这时候系统运行时,系统CPU利用率不高,都是IO读写的操作。IO密集型程序通常在达到性能极限时(主要和硬盘的读写速度有关),CPU占用率还是很低,可能是因为任务本身需要大量的IO操作,比如读写文件、传输文件、网络请求,DB读取,大部分Web应用都是IO密集型。

3、合理设置核心线程数

对于CPU密集型任务,由于CPU密集型任务的性质,导致CPU的使用率很高,如果线程池中的核心线程数量过多,会增加上下文切换的次数,带来额外的开销。因此,一般情况下线程池的核心线程数量等于CPU核心数+1。

对于I/O密集型任务,由于I/O密集型任务CPU使用率并不是很高,可以让CPU在等待I/O操作的时去处理别的任务,充分利用CPU。因此,一般情况下线程的核心线程数等于CPU核心数*2。

对于混合型任务,由于包含2种类型的任务,故混合型任务的线程数与线程时间有关。一般情况下:线程池的核心线程数=(线程等待时间/线程CPU时间+1)*CPU核心数;在某种特定的情况下还可以将任务分为I/O密集型任务和CPU密集型任务,分别让不同的线程池去处理。

Runtime.getRuntime().availableProcessors() // 获取系统CPU数量

ThreadPoolExecutor 的运行流程

任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

  1. 直接申请线程执行该任务
  2. 缓冲到队列中等待线程执行
  3. 拒绝该任务

线程管理部分充当消费者的角色,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

线程池的运行状态

线程池运行的状态是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值,运行状态(runState)和线程数量(workerCount)。

  • running:能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • shutdown:指调用了 shutdown() 方法,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。
  • stop:指调用了 shutdownNow() 方法,不再接受新提交的任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
  • tidying: 所有任务都执行完毕,workerCount 有效线程数为 0,线程池中没有线程了。
  • terminated:终止状态,当执行 terminated() 后会更新为这个状态。

线程池的任务调度

首先,所有任务的调度都是由 execute 方法完成的,比如我们业务代码中:threadPool.execute(new Job());

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //1.当前池中线程比核心数少,新建一个线程执行任务
    if (workerCountOf(c) < corePoolSize) {   
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2.核心池已满,但任务队列未满,添加到队列中
    if (isRunning(c) && workQueue.offer(command)) {   
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))    //如果这时被关闭了,拒绝任务
            reject(command);
        else if (workerCountOf(recheck) == 0)    //如果之前的线程已被销毁完,新建一个线程
            addWorker(null, false);
    }
    //3.核心池已满,队列已满,试着创建一个新线程
    else if (!addWorker(command, false))
        reject(command);    //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}

这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。

如果 workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

如果 workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中

如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

如果 workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。

待执行任务的队列

待执行任务的队列是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

下图中展示了 Thread1 往阻塞队列中添加元素,而线程 Thread2 从阻塞队列中移除元素

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize 时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

常见的线程池

FixedThreadPool(有限线程数的线程池)

CachedThreadPool (无限线程数的线程池)

ScheduledThreadPool (定时线程池)

SingleThreadExecutor (单一线程池)

ForkJoinPool

ForkJoinPool线程池最大的特点就是分叉(fork)合并(join),将一个大任务拆分成多个小任务,并行执行,再结合工作窃取模式(worksteal)提高整体的执行效率,充分利用CPU资源。

总结

1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面

有任务,线程池也不会马上执行它们。

2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要

创建非核心线程立刻运行这个任务;

d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池

会抛出异常 RejectExecutionException。

3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运

行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它

最终会收缩到 corePoolSize 的大小

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值