Java线程池

内容纲要

1.线程池简介

线程池的重要性:不言而喻

如果不使用线程池

  • 每个任务都需要开一个线程处理
    • 一个线程
    • for循环创建线程
    • 当任务数量上升到1000
  • 缺点: 这样开销太大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题。创建个销毁会有很大的代价,需要jvm辅助很多工作

为什么要使用线程池

  • 问题一: 反复创建开销大
  • 问题二: 过多的线程会占用太多内存
  • 解决以上两个问题的思路
    • 用少量的线程--避免内存占用过多(内存占用过多会给GC带来压力)
    • 让这部分线程都保持工作,且可以反复执行任务---避免生命周期的损耗

线程池的好处

  • 加快响应速度,消除了线程创建销毁带来的延迟之后,对用户体验也是增强的
  • 合理利用CPU和内存
  • 统一管理,方便数据统计

线程池适合应用的场合

  • 服务器接受到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率(Tomcat服务器为经典代表)
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

2.创建和停止线程池

线程池构造函数的参数

corePoolSize 和 maxPoolSize

  • corePoolSize指的是核心线程数:线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时再创建新线程去执行任务
  • 线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,这就是最大量maxPoolSize

  • 添加线程规则
    1. 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。
    2. 如果线程数等于(或大于)corePoolSize但少于maximumPoolSize,则将任务放入队列。
    3. 如果队列已满,并且线程数小于maxPoolSize,则创建个新线程来运行任务
    4. 如果队列已满,并且线程数大于或等于maxPoolSizeA则拒绝该任务。

  • 举个例子
    1. 线程池:核心池大小为5,最大池大小为10,队列为100.
    2. 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建最新的线程maxPoolSize,最多到10个线程,如果再来任务,就拒绝
  • 增减线程的特点
    1. 通过设置corePoolSize和maximumPoolSize 相同,就可以创建固定大小的线程池。
    2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
    3. 通过设置maximumPoolSize为很高的值,例如Integer.MAXVALUE,可以允许线程池容纳任意数量的并 发任务
    4. 是只有在队列填满时才创建多于corePoolSize的线程,所以4如果你使用的是无界队列(例如LinkedBlockingQueue),那么线程数就不会超过corePoolSize。

keepAliveTime

  • 如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,它们就会被终止

ThreadFactory用来创建线程

  • 新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory0,创建出来的线程都在同个线程组,拥有同样的NORM PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等
  • 通常我们用默认的ThreadFactory就可以了

工作队列

  • 有3种最常见的队列类型
    1. 直接交接: SynchronousQueue,这种队列没有容量,存不了任务的
    2. 无界队列: LinkedBlockingQueue,设置maximumPoolSize是不会起作用的,可以防止流量突增,但是如果处理速度更不上放入任务的速度则会发生内存溢出和内存浪费
    3. 有界队列: ArrayBlockingQueue

线程池应该手动创建还是自动创建

线程创建更应该手动创建,根据不同的业务场景,自己设置线程池参数,比如我们的
内存有多大,我们想给线程取什么名字等等

自动创建的列子

  • newFixedThreadPool: 由于传进去的LinkedBlockingQueue是没有容量上限的所以当请求数越来越多,并且无法及时处理完毕的时候也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。
  • newSingleThreadExecutor:这里和刚才的newFixedThreadPool的原理基本一样,只不过把线程数直接设置成了1,所以这也会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存。
  • newCachedThreadPoo:
    • 可以缓存线程池
    • 特点: 无界线程池,具有自动回收多余线程的功能
    • 这里的弊端在于第二个参数maximumPoolSize被设置为了Integer.MAXVALUE,这可能会创建数量非常多的线程甚至导致OOM。
  • newScheduledThreadPoo:
    • 支持定时及周期性任务执行的线程池
    • 各种延时任务,定时任务可以使用

线程池里的线程数量设定为多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法。
  • 线程数=*CPU核心数(1+平均等待时间/平均工作时间)**
  • 最终需要进行压测,然后得出需要的线程数量

停止线程池的正确方法

  1. shutdown: 执行后需要把正在运行的任务执行完毕,新的任务则就会拒接执行了
  2. isShutdown: 返回boolean值,表示当前线程池是否进入到了停止状态、
  3. isTerminated: 返回当前线程是否已经完全终止了
  4. awaitTermination: 用来测试一段时间线程是否会停止的方法,主要功能是检测,返回的情况: 所有任务都执行完毕;等待的时间到了;等待的时候被中断会抛出异常;
  5. shutdownNow: 立刻关闭任务,正在执行的线程收到中断信号开始进行停止线程的工作,队列中的等待线程则会直接返回,返回值是一个列表,队列中等待的任务

3.常见线程池的特点和用法

4.任务太多,怎么拒绝?

拒绝时机

  1. 当Executor关闭时,提交新任务会被拒绝
  2. 以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时

4种拒绝策略

  • AbortPolicy:直接拒绝,抛出异常
  • DiscardPolicy:默默丢弃任务,不会有通知
  • DiscardOldestPolicy:丢弃最老的任务
  • CallerRunsPolicy:让提交线程去执行,可以让提交的速度降低,是一种负反馈

5.实现原理,源码分析

线程池组成部分

  • 线程池管理器
  • 工作线程
  • 任务队列
  • 任务接口(Task)

线程池状态

  • RUNNING:接受新任务并处理排队任务
  • SHUTDOWN:不接受新任务,但处理排队任务
  • STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务
  • TIDYING,中文是整洁,理解了中文就容易理解这个状态了所有任务都已终止,workerCount为零时,线程会转换到
  • TIDYING状态,并将运行terminate ()钩子方法
  • TERMINATED : terminate () 运行完成

7.使用线程池的注意点

  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄漏
THE END
分享
二维码
< <上一篇
下一篇>>