线程池

📋 线程池的七大参数ThreadPoolExecutor
  • corePoolSize:常驻核心线程数,也就是说
    这是线程池中始终存活的线程,不会被销毁,除非关闭线程池。
  • maximumPoolSize:最大线程数
    线程池中最多能存活的线程数,这个值必须大于1
  • keepAliveTime:空闲线程存活时间,配合下一个参数unit(空闲线程存活时间单位)使用。
    假如keepAliveTime=5L(注意:类型为long),unit = TimeUnit.SECONDS,则某个线程在5s时间里没有处理任何请求,将会被线程池销毁,当然是在线程池中线程数大于常驻核心线程数的情况下。
  • unit:空闲线程存活时间的单位
    如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等
  • workQueue:工作队列(阻塞队列)
    队列的容量决定了能有多少请求被阻塞队列接受。线程池达到最大线程数在处理请求,接下来的请求就会被放入阻塞队列等待处理。
    如果没有阻塞队列,线程池会怎么样? #TODO
  • threadFactory:线程工厂
    一般使用默认Executors.defaultThreadFactory()即可。
  • handler:线程池的拒绝策略。
    当等待队列已经满了,再也塞不下新的任务,同时,线程池也达到max线程了,无法为新任务服务,这时,我们需要拒绝策略机制合理的处理这个问题。
📋 线程池的内部工作原理ThreadPoolExecutor

有了以上定义好的数据,下面来看看内部是如何实现的 。 Doug Lea 的整个思路总结起来就是 5 句话:

  • 如果当前运行的线程小于核心线程数,创建新的线程执行任务。
  • 如果当前运行的线程大于核心线程数,而且阻塞队列没有满,则将任务放入阻塞队列。
  • 如果当前线程数大于核心线程数,且阻塞队列已满,且小于最大线程数,则会创建新的线程执行该任务。
  • 如果当前运行的线程大于核心线程数,且阻塞队列已满,而且最大线程数已用完,则使用拒绝策略来拒绝新的任务。

线程池默认的拒绝策略是抛出异常,总共有四种线程池的拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认)

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务

ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

线程池里的每一个线程完成后不会立即退出,而是去检查阻塞队列是否还有任务需要执行,如果没有任务,那么线程就会退出。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yGtSDv8-1640144949027)(image/image.png)]

📋 在实际工作中使用的是哪一个线程池

超级大坑警告

Q:你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?

A:一个都不用,我们生产环境是哪个只能使用自定义的。

Q:为什么不用?系统提供的而使用自定义的呢?

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

📋 线程池配置合理线程数

分两个方面进行考虑

  • CPU密集型
    CPU核数+1个线程的线程池
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:
  • IO密集型
    CPU核心数/1-阻塞系数
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。

IO密集型,即该任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。

所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU核数/ (1-阻塞系数)

阻塞系数在0.8~0.9之间

比如8核CPU:8/(1-0.9)=80个线程数
谈谈对线程安全的理解

当多个线程访问一个对象时,如果不用进行额外的同步控制或者协调操作,调用这个对象的行为都可以获得正确的结果,我问就说这个对象是线程安全的

如何实现线程安全?

实现线程安全的方式有很多种,在源码中最常见的是synchronize关键字给代码块或者方法加锁,比如StringBuffer,查看StringBuffer源码就是加锁保证线程安全的。

开发中需要拼接字符串应该使用StringBuffer还是StringBuilder

场景一:如果使用的是非线程安全的对象StringBuider,那么就需要借助外力,给他加synchronized关键字,或者直接使用线程安全的对象StringBuffer.

场景二:如果每个线程访问的是各自的资源 ,那么就不需要考虑线程安全的问题,所以这个时候可以放心的使用非线程安全对象,比如StringBuilder

如果我们是在方法中使用,那么建议在方法中创建StingBuilder,这时候相当于是每个线程独立占有一个StringBuilder对象,不存在多个线程共享一个资源的情况,所以我们可以安心的使用,虽然StringBuilder本身不是线程安全的。

上个项目中,导出功能,就是因为在方法外创建了一个静态的String对象,通过SQL的拼接实现数据的导出,导致导出时,出现Sql拼接错误,线程不安全的问题。

解决方案就是,方法内加锁,等待SQL拼接完毕,开启异步执行导出,结束当前线程任务,然后放开锁,执行下一个任务,提高效率,完成导出。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vViNqYne-1640144949028)(image/image_1.png)]

什么时候考虑线程安全问题?

1、多个线程访问同一资源时

2、资源是有状态的,比如我们上述讲的字符串拼接,这个时候数据是会有变化的