深入了解线程池

深入了解线程池

1、线程池

线程池介绍

  • 线程池,顾名思义就是一个线程缓存,线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此Java中提供线程池对线程进行统一分配、调优和监控
  • 作用:线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
  • 优点:
    • 线程复用。减少线程创建,消亡的开销,提高性能;
    • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 管理线程。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池核心参数

  • corePoolSize
    • 线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
    • 参数设置:
      • 每个任务需要tasktime秒处理,则每个线程每秒可以处理1/tasktime个任务。系统每秒有tasks
        个任务,所以需要的线程数为tasks/(1/tasktime),即taskstasktime个线程数。
      • 假设系统每秒任务数为100-1000,每个任务耗时0.1秒,那么需要10-100个线程, 核心线程数应设
        置为大于10,具体数字最好根据八二原则,即80%情况下系统每秒任务数,若系统80%情况下每
        秒任务数小于200,最多时为1000,那么可以设置为20。
  • maximumPoolSize
    • 线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
    • 参数设置:
      • (最大任务数 - queueCapacity )*(任务耗时)
      • CPU密集型程序:线程数等于 CPU 核心数 + 1;
      • IO密集型程序:线程数等于 CPU 核心数 * 2;
  • keepAliveTime
    线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
  • unit
    • keepAliveTime的单位
  • workQueue
    • 用来保存等待被执行的任务的阻塞队列。任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
      1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
      2. LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
      3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
      4. priorityBlockingQuene:具有优先级的无界阻塞队列;
  • threadFactory
    • 用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
  • handler
    • 线程池的饱和拒绝策略。当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
      1. AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
      2. CallerRunsPolicy:用调用者所在的线程来执行任务;
      3. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务;
      4. DiscardPolicy:丢弃任务,但是不抛出异常;
      5. 也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

线程池五大状态

  • image-20220512163740133

  • RUNNING

    • 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    • 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
  • SHUTDOWN

    • 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    • 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
  • STOP

    • 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    • 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
  • TIDYING

    • 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  • TERMINATED

    • 状态说明:线程池彻底终止,就变成TERMINATED状态。
    • 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
    • 进入TERMINATED的条件如下:
      • 线程池不是RUNNING状态;
      • 线程池状态不是TIDYING状态或TERMINATED状态;
      • 如果线程池状态是SHUTDOWN并且workerQueue为空;
      • workerCount为0;
      • 设置TIDYING状态成功。

线程池底层工作原理

image-20220512171216883

image-20220512170944065

  1. 在创建了线程池后,开始等待请求。
  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
    3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心
      线程立刻运行这个任务;
    4. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝
      策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
  5. 线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

线程池四大线程池

  1. FixedThreadPool : 该⽅法返回⼀个固定线程数量的线程池。适合任务量比较固定但耗时长的任
    务。
  2. SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。这类线程池适用于多个任务顺序
    执行的场景。
  3. CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。适合任务量大但
    耗时少的任务。
  4. ScheduledThreadPool :该方法返回一个能实现定时、周期性任务的线程池。适合执行定时或周
    期性的任务。核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延
    时阻塞队列。

缺点:

  • FixedThreadPool和SingleThreadExecutor允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM;
  • CachedThreadPool和ScheduledThreadPool 允许创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM;

阿里巴巴开发手册推荐自定义线程池

线程池如何证线程一直运行的

  • 在当前线程执行的runWorker方法中有个while循环,while循环的第一个判断条件是执行当前线程关联的Worker对象中的任务,执行一轮后进入while循环的第二个判断条件getTask(),从任务队列中取任务,取这个任务的过程要么是一直阻塞的,要么是阻塞一定时间直到超时才结束的,超时到了的时候这个线程也就走到了生命的尽头。

线程池execute()方法和submit()方法

  • execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
  • submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个Future 对象可以判断任务是否执⾏成功;

2、线程池核心方法、类

execute方法

  • image-20220512173304192
  • 执行execute()方法时执行过程
    1. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
    2. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
    3. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;
    4. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
  • 要注意一下addWorker(null, false); 也就是创建一个线程,但并没有传入任务,因为任务已经被添加到workQueue中了,所以worker在执行的时候,会直接从workQueue中获取任务。所以,在workerCountOf(recheck) == 0 时执行 addWorker(null, false); 也是为了保证线程池在RUNNING状态下必须要有一个线程来执行任务。

addWorker方法

  • addWorker方法的主要工作在线程池中创建一个新的线程并执行。firstTask参数 用于指定新增的线程执行的第一个任务,core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize;

Worker类

  • 线程池中的每一个线程被封装成一个Worker对象,ThreadPool维护的其实就是一组Worker对象;
  • Worker类继承了AQS,并实现了Runnable接口,注意 firstTask 和 thread 属性:firstTask用它来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。
  • 在调用构造方法时,需要把任务传入,这里通过 getThreadFactory().newThread(this); 来新建一个线程,newThread 方法传入的参数是this,因为Worker本身继承了Runnable接口,也就是一个线程,所以一个Worker对象在启动的时候会调用Worker类中的run方法
  • Worker继承了AQS,使用AQS来实现独占锁的功能。
    1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
    2. 如果正在执行任务,则不应该中断线程;
    3. 如果该线程现在不是独占锁的状态(state = 0,即空闲的状态),说明它没有在处理任务,这时可以对该线程进行中断;
    4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;
    5. 之所以不可重入,是因为不希望任务在调用像 setCorePoolSize 这样的线程池控制方法时重新获取锁。如果使用ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。所以,Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。

runWorker方法

  • runWorker方法的执行过程:
    1. while循环不断地通过getTask()方法获取任务;
    2. getTask()方法从阻塞队列中取任务;
    3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
    4. 调用task.run()执行任务;
    5. 如果task为null则跳出循环,执行processWorkerExit()方法;
      • 该方法会根据 allowCoreThreadTimeOut 属性判断是否删除核心线程,至少保留一个;
    6. runWorker方法执行完毕,也代表着Worker中的run方法执行完毕,销毁线程。

工作线程的生命周期

image-20220512175257241

  1. execute方法开始;
  2. Worker使用ThreadFactory创建新的工作线程;
  3. runWorker通过getTask获取任务,然后执行任务;
  4. 如果getTask返回null,进入processWorkerExit方法,整个线程结束;

3、CPU密集型与IO密集型

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的时间。

  • 线程数一般设置为:线程数 = CPU核数 + 1

IO密集型(I/O bound)

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

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

  • 线程数一般设置为:线程数 = ((线程等待时间+线程CPU时间)/ 线程CPU时间 )* CPU数目

CPU密集型 vs IO密集型

  • 可以把任务分为计算密集型和IO密集型。
  • 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
  • 计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
  • 第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。
  • 常见的大部分任务都是IO密集型任务,比如Web应用。IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
  • 对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值