聊一聊线程池

2024-06-12 22:28
文章标签 线程 聊一聊

本文主要是介绍聊一聊线程池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为什么要使用线程池

我们经常听说一些类似连接池,对象池的概念,它们本质上都是对某种资源进行池化的处理,以提升资源的整体的使用效率。是否要将某项资源进行池化处理,主要需要考虑以下几方面的因素:

  1. 资源的创建的时间或者空间开销是不是比较大;
  2. 资源是否会频繁的使用;
  3. 资源本身是否为无状态的(相对意义上的无状态,指的是使用资源不会更改资源的内部状态)

是否要使用线程池,其实也是对照以上三条来看的:对于第一条线程无疑是满足的,虽然称之为轻量化的进程,但是创建线程依然是一项很重的操作。另外多个任务复用同一线程,能够减少线程上下文切换的开销,提升响应时间。但是第二,第三条就要仔细考虑了,如果你的任务本身就是一次性的,或者很长的周期才跑一次,那么用不用线程池其实没啥差别,反而可能因为缓存了额外不需要的线程而耗费内存资源;另外更重要的是,如果你的任务依赖于线程中保存的某些状态(比如java的ThreadLocal),那么使用线程池就要非常小心了,你必须在每次任务结束时小心的清理状态,否则下一个任务可能就会运行到受污染的线程上下文中。

另外线程池的使用还可以增加对线程的可管理性,因为线程池除了复用线程外,还有个重要的作用就是限制对线程的创建,对于线程这种重量级的资源,如果任由应用进行创建而没有抑制的手段,本身就是很危险的。

Java中的线程池

Java语言中为线程池提供了一个标准的抽象接口 ExecutorService,我们这里不谈具体用法,主要聊一些核心概念。ExecutorService有一个通用的实现类,叫做ThreadPoolExecutor,我们通过Executors.newXXX创建的线程池,多数都是调用ThreadPoolExecutor的不同入参的构造器来实现的。我们先来看看ThreadPoolExecutor参数最多的那个构造器:

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

这个构造器的入参其实已经说明了线程池的一些重要概念,前4个参数我们这里就略过了,这是池化技术常用的一些参数,也比较容易理解。我们主要来看看workQueue,threadFactory和rejectedHandler 。

任务队列

其实就是任务的等待队列,当你往线程池里面提交一个任务时,有可能当前所有的线程都处于满负荷状态,无法立即处理你提交的任务,这个时候就需要一个队列将所有无法立即处理的任务缓存起来,等待有线程空闲后再提交给线程执行。既然要被线程池中所有的线程共享,那么该队列必然是线程安全的。根据不同的需求BlockingQueue有几种常用的实现:

实现类特点
ArrayBlockingQueue基于数组的阻塞队列,使用数组存储数据,并需要指定其长度,所以是一个有界队列
LinkedBlockingQueue基于链表的阻塞队列,使用链表存储数据,默认是一个无界队列;也可以通过构造方法中的capacity设置最大元素数量,所以也可以作为有界队列
SynchronousQueue一种没有缓冲的队列,提交的任务会立即被执行,但如果没有空闲线程,则会阻塞
PriorityBlockingQueue基于优先级别的阻塞队列,底层基于数组实现,是一个无界队列
DelayQueue延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队

我们通常使用较多的主要是 LinkedBlockingQueueArrayBlockingQueue,LinkedBlockingQueue是ThreadPoolExecutor默认的任务队列,由于入队和出队使用了分离的两把锁,理论上并发性能更好。但由于它默认创建时是不限制队列大小的,使用的时候尤其需要小心,如果任务的生成速率远高于线程池的消费速率,就可能在队列中产生大量的对象堆积,最终导致OOM。所以在阿里的java开发规范手册里面,也是不推荐直接使用Executors.newXXX创建线程池的,我们通常使用时还是应该根据实际需要为队列指定一个最大长度,防止极端情况下的系统崩溃。
SynchronousQueue主要用在那种需要任务提交后立即被执行的场合,所以它是无缓冲的,Executors.newCachedThreadPool() 底层的BlockingQueue使用的就是SynchronousQueue,因为CachedThreadPool在线程数不足的时会自动创建新的线程,也就没有设置缓存队列的必要了。
DelayQueueScheduledExecutorService底层使用的缓冲队列,它也是无界的,队内的元素会按照执行时间进行排序,最近需要执行的任务排在对头,非常适合需要按照时间进行任务调度的场景。但是正是因为需要排序,每次入队新的元素,都会对整个队列进行重新排序,性能相对较低一些。

线程工厂

既然是线程池,那肯定需要创建线程的,而如何创建线程,就是ThreadFactory要做的事情,一个典型的ThreadFactory的实现如下:

 ThreadFactory threadFactory = new ThreadFactory() {private final AtomicInteger index = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setName("worker-" + index.getAndIncrement());t.setDaemon(true);return t;}};

它最大的作用就是对线程池中的线程进行某种程度的定制,比如线程的优先级,线程的名称。我觉得线程的名称尤其重要,推荐每个特定业务用途的线程池都要自定义内部线程的名称,这样通过threaddump或者其它线程监控工具,可以一目了然的看出每个线程的用途,不然满篇的thread-pool-3这些名字,看着还是很让人崩溃的…

拒绝处理器

RejectedExecutionHandler的作用是当提交一个任务,而线程池当前无法接受时(可能有很多原因,比如线程池已经被关闭了,或者线程池的缓冲队列已经满了),应该如何处理这个任务。Java中内置了几种常用的拒绝策略:

//默认策略, 这个策略会抛出一个 RejectedExecutionException 来拒绝新任务的处理。
ThreadPoolExecutor.AbortPolicy;
//这个策略不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
ThreadPoolExecutor.CallerRunsPolicy//这个策略将静默地忽略无法处理的任务,不抛出异常,也不提供任何通知。
ThreadPoolExecutor.DiscardPolicy//这个策略将抛弃队列中最老的一个请求,尝试再次提交当前任务
ThreadPoolExecutor.DiscardOldestPolicy

拒绝策略通常可以用作系统过载时的熔断保护,比如自动丢弃无法被及时处理的新的请求,并记录警告日志;当然如果任务不允许丢弃,你也可以自行实现一个RejectedExecutionHandler,将无法被及时处理的任务持久化到数据库或者其它什么地方,在系统空闲时再尝试取出来进行处理。

线程池的调优

除了刚才所说的在几个核心概念中根据业务场景选择合适的实现类之外,线程池最重要的调优手段就是线程池中线程个数的选择。线程数太少无法满足并发任务的需求,而太多的线程会大大的增加线程上下文的切换开销,反而降低内存。而且一个线程会占用大约1~2M的栈空间,过多的线程也会增加系统整体的资源消耗。根据业务场景,我们的任务一般会分为CPU密集型和IO密集型两种,这两种类型的任务对于线程数的配置是有所差异的:

CPU密集型 这类任务的特点是需要大量的CPU计算资源,很少发生阻塞或等待事件,比如数值运算,加解密,编解码等。对于这类任务,理想的线程数应该与处理器的核心数相匹配,或者略高于核心数(通过Runtime.getRuntime().availableProcessors()获取)。
IO密集型 任务则是频繁进行IO操作,如文件操作、网络数据传输等,这类任务的线程经常处于等待状态。其核心线程数可以设置为CPU核心数的两倍到三倍。具体数值可以根据实际的阻塞系数来调整。阻塞系数是指在任务总运行时间中,花费在等待IO操作上的时间比例.
计算方法:可以使用公式 N_threads = N_cpu * U_cpu * (1 + W/C) 来估算,其中:
N_cpu: 是CPU核心数
U_cpu: 是期望的CPU利用率(通常为1)
W/C: 是等待时间与计算时间的比率

不过以上规则都是纸面上的,我们大多数实际的工作任务都是CPU和IO的混合型任务,很难直接根据经验给出合适的值,最佳的方式是对线程池的使用进行持续的监控,根据监控结果来动态的调整线程池的各项参数。这里推荐一个开源的线程池监控框架 dynamic-tp 不仅能够无感的对线程池的使用情况进行监控,而且结合线上的配置中心(支持多种常用的配置中心中间件)实现线程池参数的动态调整,而无需对服务进行重启。

最后,还是要告诫大家,不要做无目的的优化,很多时候你以为的性能问题都不是问题,盲目优化只会让你的系统架构更加复杂,更难以维护。我们需要先通过观测收集数据,证明确实在某个方面存在性能瓶颈,再有的放矢的去做优化。所以,优化的前提还是先努力的提升系统的可观测性吧。

这篇关于聊一聊线程池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1055514

相关文章

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

java线程深度解析(二)——线程互斥技术与线程间通信

http://blog.csdn.net/daybreak1209/article/details/51307679      在java多线程——线程同步问题中,对于多线程下程序启动时出现的线程安全问题的背景和初步解决方案已经有了详细的介绍。本文将再度深入解析对线程代码块和方法的同步控制和多线程间通信的实例。 一、再现多线程下安全问题 先看开启两条线程,分别按序打印字符串的

java线程深度解析(一)——java new 接口?匿名内部类给你答案

http://blog.csdn.net/daybreak1209/article/details/51305477 一、内部类 1、内部类初识 一般,一个类里主要包含类的方法和属性,但在Java中还提出在类中继续定义类(内部类)的概念。 内部类的定义:类的内部定义类 先来看一个实例 [html]  view plain copy pu

C#线程系列(1):BeginInvoke和EndInvoke方法

一、线程概述 在操作系统中一个进程至少要包含一个线程,然后,在某些时候需要在同一个进程中同时执行多项任务,或是为了提供程序的性能,将要执行的任务分解成多个子任务执行。这就需要在同一个进程中开启多个线程。我们使用 C# 编写一个应用程序(控制台或桌面程序都可以),然后运行这个程序,并打开 windows 任务管理器,这时我们就会看到这个应用程序中所含有的线程数,如下图所示。