一、前言

对于从事后端开发的同学来说,线程是必须要使用了,因为使用它可以提升系统的性能。但是,创建线程和销毁线程都是比较耗时的操作,频繁的创建和销毁线程会浪费很多CPU的资源。

此外,如果每个任务都创建一个线程去处理,这样线程会越来越多。我们知道每个线程默认情况下占1M的内存空间,如果线程非常多,内存资源将会被耗尽。这时,我们需要线程池去管理线程,不会出现内存资源被耗尽的情况,也不会出现频繁创建和销毁线程的情况,因为它内部是可以复用线程的。

 

二、从实战开始

在介绍线程池之前,让我们先看个例子。

public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {          System.out.println("MyCallable call");return "success";    }
public static void main(String[] args) {ExecutorService threadPool = Executors.newSingleThreadExecutor();try {Future<String> future = threadPool.submit(new MyCallable());            System.out.println(future.get());        } catch (Exception e) {           System.out.println(e);        } finally {            threadPool.shutdown();        }}
}

这个类的功能就是使用Executors类的newSingleThreadExecutor方法创建了的一个单线程池,他里面会执行Callable线程任务。

三、创建线程池的方法

我们仔细看看Executors类,会发现它里面给我们封装了不少创建线程池的静态方法,如下图所示:

其实,我们总结一下其实只有6种:

1.newCachedThreadPool可缓冲线程池

它的核心线程数是0,最大线程数是integer的最大值,每隔60秒回收一次空闲线程,使用SynchronousQueue队列。SynchronousQueue队列比较特殊,内部只包含一个元素,插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。

2.newFixedThreadPool固定大小线程池

它的核心线程数 和 最大线程数是一样,都是nThreads变量的值,该变量由用户自己决定,所以说是固定大小线程池。此外,它的每隔0毫秒回收一次线程,换句话说就是不回收线程,因为它的核心线程数 和 最大线程数是一样,回收了没有任何意义。此外,使用了LinkedBlockingQueue队列,该队列其实是有界队列,很多人误解了,只是它的初始大小比较大是integer的最大值。

3.newScheduledThreadPool定时任务线程池

它的核心线程数是corePoolSize变量,需要用户自己决定,最大线程数是integer的最大值,同样,它的每隔0毫秒回收一次线程,换句话说就是不回收线程。使用了DelayedWorkQueue队列,该队列具有延时的功能。

4.newSingleThreadExecutor单个线程池

其实,跟上面的newFixedThreadPool是一样的,稍微有一点区别是核心线程数 和 最大线程数 都是1,这就是为什么说它是单线程池的原因。

5.newSingleThreadScheduledExecutor单线程定时任务线程池

该线程池是对上面介绍过的ScheduledThreadPoolExecutor定时任务线程池的简单封装,核心线程数固定是1,其他的功能一模一样。

6.newWorkStealingPool窃取线程池

它是JDK1.8增加的新线程池,跟其他的实现方式都不一样,它底层是通过ForkJoinPool类来实现的。会创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。

讲了这么多,具体要怎么用呢?

其实newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 和 newWorkStealingPool方法创建和使用线程池的方法是一样的。这四个方法创建线程池返回值是ExecutorService,通过它的execute方法执行线程。

public class MyWorker implements Runnable {@Override
public void run() {System.out.println("MyWorker run");}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(8);
try {threadPool.execute(new MyWorker());} catch (Exception e) {System.out.println(e);} finally {threadPool.shutdown();}}
}

newScheduledThreadPool 和 newSingleThreadScheduledExecutor 方法创建和使用线程池的方法也是一样的

public class MyTask implements Runnable {@Override
public void run() {System.out.println("MyTask call");}public static void main(String[] args) {ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
try {scheduledExecutorService.schedule(new MyRunnable(), 60, TimeUnit.SECONDS);} finally {scheduledExecutorService.shutdown();}}
}

以上两个方法创建的线程池返回值是ScheduledExecutorService,通过它的schedule提交线程,并且可以配置延迟执行的时间。

 

四、自定义线程池

Executors类有这么多方法可以创建线程池,但是阿里巴巴开发规范中却明确规定不要使用Executors类创建线程池,这是为什么呢?

newCachedThreadPool可缓冲线程池,它的最大线程数是integer的最大值,意味着使用它创建的线程池,可以创建非常多的线程,我们都知道一个线程默认情况下占用内存1M,如果创建的线程太多,占用内存太大,最后肯定会出现内存溢出的问题。

newFixedThreadPool和newSingleThreadExecutor在这里都称为固定大小线程池,它的队列使用的LinkedBlockingQueue,我们都知道这个队列默认大小是integer的最大值,意味着可以往该队列中加非常多的任务,每个任务也是要内存空间的,如果任务太多,最后肯定也会出现内存溢出的问题。

阿里建议使用ThreadPoolExecutor类创建线程池,其实从刚刚看到的Executors类创建线程池的newFixedThreadPool等方法可以看出,它也是使用ThreadPoolExecutor类创建线程池的。

从上图可以看出ThreadPoolExecutor类的构造方法有4个,里面包含了很多参数,让我们先一起认识一下:

corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:空闲线程回收时间间隔
unit:空闲线程回收时间间隔单位
workQueue:提交任务的队列,当线程数量超过核心线程数时,可以将任务提交到任务队列中。比较常用的有:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue;
threadFactory:线程工厂,可以自定义线程的一些属性,比如:名称或者守护线程等
handler:表示当拒绝处理任务时的策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

我们根据上面的内容自定义一个线程池:

public class MyThreadPool implements  Runnable {private static final ExecutorService  executorService = new ThreadPoolExecutor(
8,
10,
30,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
new ThreadPoolExecutor.AbortPolicy());@Override
public void run() {System.out.println("MyThreadPool run");}public static void main(String[] args) {
int availableProcessors = Runtime.getRuntime().availableProcessors();
try {executorService.execute(new MyThreadPool());} catch (Exception e) {System.out.println(e);} finally {executorService.shutdown();}}
}

从上面可以看到,我们使用ThreadPoolExecutor类自定义了一个线程池,它的核心线程数是8,最大线程数是 10,空闲线程回收时间是30,单位是秒,存放任务的队列用的ArrayBlockingQueue,而队列满的处理策略用的AbortPolicy。使用这个队列,基本可以保持线程在系统的可控范围之内,不会出现内存溢出的问题。但是也不是绝对的,只是出现内存溢出的概率比较小。

当然,阿里巴巴开发规范建议不使用Executors类创建线程池,并不表示它完全没用,在一些低并发的业务场景照样可以使用。

五、最佳线程数

在使用线程池时,很多同学都有这样的疑问,不知道如何配置线程数量,今天我们一起探讨一下这个问题。

1.经验值

配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?

什么是IO密集型?

比如:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。

什么是CPU密集型?

比如:非常复杂的调用,循环次数很多,或者递归调用层次很深等。

IO密集型配置线程数经验值是:2N,其中N代表CPU核数。

CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。

如果获取N的值?

int availableProcessors = Runtime.getRuntime().availableProcessors();

那么问题来了,混合型(既包含IO密集型,又包含CPU密集型)的如何配置线程数?

混合型如果IO密集型,和CPU密集型的执行时间相差不太大,可以拆分开,以便于更好配置。如果执行时间相差太大,优化的意义不大,比如IO密集型耗时60s,CPU密集型耗时1s。

2.最佳线程数目算法

除了上面介绍是经验值之外,其实还提供了计算公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合系统压测,基本可以确定最适合的线程数。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

线程池最佳线程数量到底要如何配置?相关推荐

  1. 易语言mysql线程池数量_线程池最佳线程数量到底要如何配置?

    前言 对应从事后端开发的同学来说,线程是必须要使用了,因为使用它可以提升系统的性能.但是,创建线程和销毁线程都是比较耗时的操作,频繁的创建和销毁线程会浪费很多CPU的资源.此外,如果每个任务都创建一个 ...

  2. java统计系统线程数_Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  3. java线程池最大线程数_Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  4. 今天我们不聊原理,能拿来即用的线程池最佳实践

    这篇文章篇幅在5000字左右,绝对是干货.标题稍微有点夸张,嘿嘿,实际都是自己使用线程池的时候总结的一些个人感觉比较重要的点. 为什么要使用线程池? " 池化技术相比大家已经屡见不鲜了,线程 ...

  5. Python 多线程总结(2)— 线程锁、线程池、线程数量、互斥锁、死锁、线程同步

    主要介绍使用 threading 模块创建线程的 3 种方式,分别为: 创建 Thread 实例函数 创建 Thread 实例可调用的类对象 使用 Thread 派生子类的方式 多线程是提高效率的一种 ...

  6. Android应用线程池最大线程数量

    Android应用线程池最大线程数量 线程池的大小经验值一般这样设置:(其中N为CPU的核数)  如果是CPU密集型应用,则线程池大小设置为N+1  如果是IO密集型应用,则线程池大小设置为2N+1 ...

  7. 深入理解-CPU核心数与线程池并发线程数关系

    那是一个风和日丽的下午! 面试官微微一笑,对我说:"小伙子,合理配置线程池你是如何考虑的?" 我微微一笑,说出了我的答案: 首先确认业务是CPU密集型还是IO密集型的, 如果是CP ...

  8. 为什么要使用线程池?线程池有什么作用?

    1.什么是线程池? java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池 一个线程池包括以下四个基 ...

  9. ReentrantLock+线程池+同步+线程锁

    1.并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行. 2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量 ...

最新文章

  1. 【Deep Learning笔记】用Inception-V3模型进行图像分类
  2. nginx源码分析—内存池结构ngx_pool_t及内存管理
  3. 如何在 ASP.NET Core 中为同一接口配置不同的实现
  4. C++描述杭电OJ 2011.多项式求和 ||
  5. IDEA快捷键的使用成就手速之旅(要想手速变得快,快捷练习必须刚)
  6. Microsoft Office Communications Server 2007部署以及客户端安装(详细)【一】
  7. ajax--跨域问题及三种简单的解决方案
  8. 不得不爱开源 Wijmo jQuery 插件集(13)-【Tooltip】(附页面展示和源码)
  9. IOS 中的Notification 学习
  10. 贾跃亭个人破产内幕曝光 差一点获得中东土豪投资
  11. win11拉伸屏幕_win11系统出现拉伸屏幕问题修复办法
  12. st7789 旋转_有没有人调过 ST7789V驱动的显示屏啊
  13. 有源反射系数与有源驻波比测试方法
  14. VUE记录用户切屏次数并弹出提示
  15. 有什么好用的表单工具?
  16. Java语言有哪些特点?
  17. 作为一名合格的大学生,如何在B站愉快的学习
  18. HTML标签播放MP4视频
  19. python123怎么注销账号_怎么注销账号
  20. 阿里云服务器为什么总是那么不稳定经常崩溃掉线?

热门文章

  1. windows iis 部署 django项目
  2. Django 表操作时 字段名为变量
  3. isvisible java_.NET(C#) Selenium操作调用浏览器判断页面元素(ElementIsVisible)可见的方法...
  4. Android横向滚动卡片,Android实现横向滑动卡片效果
  5. omnigraffle 画曲线_Omnigraffle画线框图的新手操作指南
  6. js怎么获取扫码枪条码_生产扫码计件解决方案
  7. 堆栈的初始化,主要是为ss和SP赋初值
  8. (计算机组成原理)第七章输入和输出系统-第一节:I/O系统基本概念和I/O控制方式简介
  9. 2-5:C++快速入门之引用,引用和指针的区别
  10. 3-2:常见任务和主要工具之存储介质