线程池大概原理

  • 为什么要使用线程池
  • 线程池的常用参数
  • corePoolSize(核心线程数量)
  • workQueue(任务队列)
  • maximumPoolSize(最大线程数)
  • RejectedExecutionHandler
  • keepAliveTime


为什么要使用线程池

线程池(ThreadPoolExecutor)顾名思义,就是类似于连接池一样,存储线程的一个"容器",方便我们从中取线程用。
而我们知道,在java中线程的创建操作就是new一个Thread,看起来很简单,那为什么还要特意放到一个池子里面去取用呢?
基于一个学习的基本思路,我们七成靠猜测三成靠取证
稍微了解一点的人很容易猜到,如果不用线程池,创建销毁线程是一个很耗资源的操作,很多涉及到一些资源的取用,比如数据库连接池之类的,都是这样的原理
上面的猜测是正确的,但是不够完全和精准。
其实主要有两点

  1. java中的线程模型是基于操作系统的原生线程模型实现的,所以线程的创建,析构与同步都要进行系统调用,这样的系统调用需要在用户态与内核中来回切换,代价很高。而线程的生命周期(创建时期,执行任务时期,销毁时期)中,创建和销毁都会进行系统调用,所以综上所述,为了减少这种代价,我们尽量避免线程处于这两个生命周期的时间,就是把线程创建好重复使用,减少频繁的创建与销毁。
  2. 我们知道了每一个java线程与系统内核线程是一对一的,所以就必然会占用一定的空间。如果一个Thread的空间是1MB,那么1024个就是1GB,在高并发的情况下,这种频繁创建线程的操作很容易发生OOM。所以利用线程池也是为了控制创建线程的数量在一定范围内。

线程池的常用参数

corePoolSize(核心线程数量)

我们知道了线程池中的线程数量是有限的,corePoolSize这个参数就是指定的线程池中的核心线程数量(可以理解为最小线程数量,或者常驻线程数量)
拿一个业务场景来举例(corePoolSize=3)。

  • 主线程(main)收到一个任务,发给线程池:来任务了,派个线程来执行一下
  • 线程池收到指令,但是第一次执行任务,线程池还没有线程,于是创建一个线程并把任务给它,此时线程数量为1
  • …过了一段时间
  • 主线程又收到一个任务,发给线程池:又来任务了,派个线程来执行一下
  • 线程池去池子里以看,线程数量为1(假设这个线程任务还没执行完),然后1小于corePoolSize,于是再创建一个线程并且把任务丢给它,此时线程数量为2
  • …又过了一段时间
  • 主线程又收到一个任务,发给线程池:又来任务了,派个线程来执行一下
  • 线程池去池子里以看,线程数量为2(一个线程执行完任务,另一个还没有),然后2小于corePoolSize。那么问题来了,此时有一个空余线程,要不要再创建新的线程呢?答案是要的,只要线程池中的线程数量小于corePoolSize,线程池就会一直创建线程,直到达到corePoolSize,主要是为了保证核心线程尽快达到我们设置的数量,这样如果之后有很多任务涌进来,这些已创建好的核心线程就可以马上准备好处理这些任务了,不需要再经过创建线程这种耗时的操作了。于是再创建一个线程并且把任务丢给它,此时线程数量为3
  • …又又过了一段时间
  • 主线程又又收到一个任务,发给线程池:又又来任务了,派个线程来执行一下
  • 此时线程池中还是三个线程,且假设有一个线程空出来了,线程池就不会继续创建线程了,因为核心线程数已经达标了,直接把任务分给空余线程

workQueue(任务队列)

我们紧接着上面的场景,如果核心线程都在任务中,然后此时又来一个任务,那么线程池会怎么做呢?
再创建一个新线程么?
可以是可以,但是万一有一个线程马上就执行完任务了,甚至新的线程还没创建完,之前的线程就已经空闲了,那么这种情况无疑造成了一种资源浪费。
所以有个workQueue这个概念。
当发生上述情况时,线程池不会着急的创建新线程,而是会把任务放在workQueue中。
有任务?暂时没空,在这里等一等。
然后等有线程空余了,会自己去队列中轮询取任务。
如果这时候再有任务进来,那继续排队。
这个时候其实会有两个问题

  1. 如果长久的没有新的任务进来,导致workQueue一直是空的,那么几个核心线程不断的去轮询是一种很蠢的行为,导致cpu资源浪费。所以workQueue 采用了阻塞队列,所谓阻塞是指,如果 workQueue 为空,则获取元素的线程会等待队列变为非空,一旦有新的任务入队列,会唤醒等待中的线程。阻塞状态的线程是不占用cpu资源的。
  2. 和上面情况相反,如果一直有源源不断的任务进来,核心线程来不及处理,也就是生产者的生产速度远大于消费者的消费速度,那么很容易发生OOM。所以这里的workQueue需要使用有界队列(设定了固定大小的队列)

maximumPoolSize(最大线程数)

紧接上文,假设maximumPoolSize=5
当我们的任务源源不断的进来,workQueue很快被撑满了,这个时候线程池会继续创建线程,这个时候创建的线程可以把它理解为临时外援的身份,应急的。
既然3个线程处理不来,那就再加,一直加到线程数量达到maximumPoolSize,已经不能再加了。
因为这个时候往往已经达到了系统最大负载。

RejectedExecutionHandler

线程数量达到最大负载了,任务队列也满了,这个时候再来任务,系统是真的无能为力了。
所以只能抛出异常RejectedExecutionException
告诉使用者,已经处理不了啦,该提示提示,该换系统换系统,该升级升级

keepAliveTime

度过了业务高峰期,请求没那么多了。
workQueue队列中的任务很快被清空了,5个线程也先后处理完手头的任务,空闲了下来。
此时线程池中有3个核心线程,2个外援线程
这个时候就没必要外援了,白白浪费资源,留下3个核心线程就够了
所以需要清掉两个外援线程。
但是!
这个身份不是固定的,不是说最后创建的两个线程就要被淘汰掉,系统中的5个线程身份其实是平等的,上面的核心线程外援线程只是为了做一个概念上的区分,方便理解。
在真的销毁额外线程的时候,是根据他们的空闲时间来的,谁的空闲时间先达到keepAliveTime,比如是10s,那么谁就被淘汰掉,直到剩下的线程数量等于corePoolSize