线程池

异步/多线程,对于一个业务方法而言,如果其中的调用链太长势必会引起程序运行时间延长,导致整个系统吞吐来量下降,而我们使用多线程方式来对该方法的调用链进行优化,对于一些耦合度不是特别高的调用关系可以直接通过多线程来走异步的方式进行处理,大大的缩短了程序的运行时长。

作用

  • 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗。
  • 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
  • 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

new Thread()

1. OOM: 如果当前方法突遇高并发情况,假设此时来了1000个请求,而按传统的网络模型是BIO,此时服务器会开1000个线程来处理这1000个请求(不考虑WEB容器的最大线程数配置),当1000个请求执行时又会发现此方法中存在new Thread();创建线程,此时每个执行请求的线程又会创建一个线程,此时就会出现1000*2=2000个线程的情况出现,而在一个程序中创建线程是需要向JVM申请内存分配的,但是此时大量线程在同一瞬间向JVM申请分配内存,此时会很容易造成内存溢出(OOM)的情况发生。

2. 资源开销与耗时: Java对象的生命周期大致包括三个阶段:对象的创建,对象的使用,对象的清除。因此,对象的生命周期长度可用如下的表达式表示:Object = O1 + O2 +O3。其中O1表示对象的创建时间,O2表示对象的使用时间,而O3则表示其清除(垃圾回收)时间。由此,我们可以看出,只有O2是真正有效的时间,而O1、O3则是对象本身的开销。当我们去创建一个线程时也是一样,因为线程在Java中其实也是一个Thread类的实例,所以对于线程而言,其实它的创建(申请内存分配、JVM向OS提交线程映射进程申请、OS真实线程映射)和销毁对资源是开销非常大的并且非常耗时的。

3. 不可管理性: 对于new Thread();的显示创建出来的线程是无法管理的,一旦CPU调度成功,此线程的可管理性几乎为零。

ThreadPoolExecutor

//创建一个定长的线程池
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>());
}
//创建一个支持周期执行任务的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

自定义线程池

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

所以在一般生产环境使用创建线程都是通过自定义线程池来使用线程资源:

public static void main(String[] args){
     // 线程工厂可通过 implements ThreadFactory接口自定义
     // 任务拒绝策略可通过  implements RejectedExecutionHandler接口自定义
     ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 0,
                    TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                    Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    for (int i = 0; i < 10;i++){
         final int num = i;
         threadPoolExecutor.execute(()->{
              System.out.println("线程:" + Thread.currentThread().getName() + "正在执行:" + num + "个任务");
        });
        System.out.println("线程池中线程数目:" + threadPoolExecutor.getPoolSize() + ",队列中等待执行的任务数目:" + threadPoolExecutor.getQueue().size() + ",已执行玩别的任务数目:"+threadPoolExecutor.getCompletedTaskCount());
    }
}

线程数和CPU

一个CPU核心,单位时间内只能执行一个线程的指令,理论上个线程只需要不停的执行指令,就可以跑满一个核心的利用率。

现代CPU基本都是多核心的,例如6核心12线程(超线程),可以简单的认为它就是12核心CPU。那么我这个CPU就可以同时做12件事,互不打扰。

如果要执行的线程大于核心数,那么就需要通过操作系统的调度了。操作系统给每个线程分配CPU时间片资源,然后不停的切换,从而实现“并行”执行的效果。

总结:

  1. 一个极端的线程(不停执行“计算”型操作时),就可以把单个核心的利用率跑满,多核心CPU最多只能同时执行等于核心数的“极端”线程数
  2. 如果每个线程都这么“极端”,且同时执行的线程数超过核心数,会导致不必要的切换,造成负载过高,只会让执行更慢
  3. I/O 等暂停类操作时,CPU处于空闲状态,操作系统调度CPU执行其他线程,可以提高CPU利用率,同时执行更多的线程
  4. I/O 事件的频率频率越高,或者等待/暂停时间越长,CPU的空闲时间也就更长,利用率越低,操作系统可以调度CPU执行更多的线程。

线程池参数配置

阻塞系数计算公式:执行该任务所需的时间与(阻塞时间+计算时间)的比值,即w/(w+c)

cpu密集型任务阻塞系数为0,IO密集型一般在0.8-0.9之间。

常规配置:

  • CPU 密集型的程序: 核心数 + 1
  • I/O 密集型的程序 :核心数 * 2

CPU密集型

CPU密集型任务是指该任务需要进行大量的运算,需要消耗CPU的大量算力,需要CPU的频繁计算,很少情况出现阻塞,所以CPU在处理该类型任务时会处于高速运转。

  • CPU密集型任务只有在真正的多核CPU机器上才能得到真正的增速(多核多线程同时处理)
  • CPU密集型任务则尽量少配置线程的数量,因为CPU在运行此类任务时几乎很少出现阻塞,所以最终如果配置的线程数太多,频繁切换线程调度反而会使得效率下降

配置公式:

  • CPU核数 * 1
  • CPU核数 * 1 + 1

IO密集型

IO密集型是指该类型任务在执行时会产生大量的IO(包含磁盘IO和网络IO),即在IO读取数据时,CPU需要等待数据的读取,CPU会处于“空闲”状态

  • 无论是在单核还是多核的CPU下,线程运行IO密集型任务都会导致浪费大量的计算资源,因为CPU在处理这类任务时,绝大时候是处于等待数据读取
  • IO密集型任务中我们可以采用多线程方式加速程序的运行,即使是单核的CPU上,我们也可以配置多个线程,因为在CPU等待数据读取的过程中,可以先切换到另外一个线程处理计算逻辑,等这边数据加载好了之后再切换回来,这种方式则可 以很好的将IO读取造成的CPU空闲时间利用起来

配置公式:

  • CPU核数 * 2

  • CPU核数 / 1 - 阻塞系数(cpu密集型任务阻塞系数为0,IO密集型一般在0.8-0.9之间)

动态监控

其实无论是CPU密集型还是IO密集型,大多数都是预估,并不是一个固定的答案,有时候需要动态调整和监控,可以借助一些开源的项目进行健康监控和报警。

目前实现的思路大多数都是基于美团的关于动态线程池监控的文章。

CPU核心数

Java 获取CPU核心数:

Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12

Linux 获取CPU核心数:

# 总核数 = 物理CPU个数 X 每颗物理CPU的核数 
# 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

# 查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l

https://juejin.cn/post/7038473601086914597

https://juejin.cn/post/7280429214608146490

Last Updated:
Contributors: 拔土豆的程序员