浅谈如何理解线程池的概念

Java线程池是我们高并发开发当中比较常用的一个解决方案。
多用于一些处理时间短暂,但是程序量大的操作环境当中,可以有效地解决线程的创建和销毁非耗资源速度慢的问题。


什么是线程?

线程是调度CPU的最小单元,也叫做轻量级进程LWP(Light Weight Process)
Java中有两种线程模型:

  1. 用户级线程(ULT)(Uer Level Thread)
  2. 内核级线程(KLT)(Kernel Level Thread)
  • ULT:用户程序实现,不依赖操作系统核心,应用提供创建,同步,调度和管理线程的函数来控制用户线程,不需要用户态/内核态的切换,速度快,内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。
  • KLT:系统内核管理线程(KLT),内核保存线程的状态和上下文的信息,线程阻塞不会引起进程阻塞。在处理器系统上多线程在多处理器上并行运行。现成的创建,调度和管理由内核完成,效率比UTL慢,比进程操作快。

市面上绝大多数的Java虚拟机都是使用的KLT,及系统内核管理线程
文字描述比较抽象,我们来画一个图描述一下ULTKLT的区别


undefined


JVM由于在用户空间,无权使用内核空间,只能调用系统开放的API(如:Linux开放的p_thread函数)去操作线程,在映射到底层的CPU上,由于调度API需要提高权限,所以会把自身状态陷入到内核态来取得权限。
用户所有的线程都会存放在线程表中,由内核统一的调度和维护。
这就是为什么会进行用户态/内核态状态切换的原因。


undefined


根据上图,JVM创建和执行线程可以列为一下这么几个步骤

  1. 线程会使用库调度器
  2. 之后陷入到内核空间
  3. 创建内核线程
  4. 内核中的线程会被维护到线程表中
  5. 由操作系统调度程序去调度
  6. CPU会根据调度算法分配时间
  7. 把没有执行完的线程写入给你高速内存区(SSP)

内核空间中有一个高速内存区SSP(程序任务运行状态段)是用于存储还没有执行完成,但是被分配的时间已经用完的线程中的数据,,等待下一次被分配到了时间后,就把保存在SSP里的上下文信息加载到CPU的缓存。

综上所述,线程是一个稀缺资源,他的创建和销毁是一个相对偏重且消耗资源的操作,而Java线程依赖于内核进程,创建线程需要进行操作系统状态切换,为避免过度消耗,我们要设法重用线程执行多个任务。


线程池就是一个线程缓存,负责对线程进行统一分配,调度与监控

他的优点有很多,最突出的优点就是:

  • 重用存在的线程,减少线程的创建,消亡所用开销,提升性能
  • 提高响应速度。当任务到达时,可以不需要等待线程的创建就可以立即执行
  • 提高线程的可管理性,可统一分配,调度和监控。

那么线程池是如何把线程统一分配调度与监控的呢?



【画图太累了,我就用的网图,图片来源视频:https://www.bilibili.com/video/av88030891


【画图太累了,我就用的网图,图片来源:https://blog.csdn.net/lchq1995/article/details/85230399


我们在NEW一个最基本的线程池的时候,会传入这么一下几个参数:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数两
keepAliveTime:空闲线程存活时间

  • corePoolSize顾名思义就是最大核心线程数量,是线程池可以同时执行的线程数量。
  • maximumPoolSize,既然有corePoolSize,那么如果corePoolSize满了怎么办呢?这时候就会用到一个队列,叫阻塞队列Block Queue
    什么是BlockQueue?它有什么特点?
    既然是Queue,那么久满足队列模型的(FIFO)原则,一端放入,另一端取出。(First In First Out)
    阻塞队列有一个特点就是:在任意时刻,不管并发量有多高,永远只有一个线程能进行队列的如对或出队,所以BlockQueue是一个线程安全的队列。
    并且如果队列满了,只能进行出队操作,所有入队操作必须等待,也就是阻塞。
    如果队列为空,那么就只能进行入队操作,所有出队操作必须等待,也就是阻塞。
    一旦线程池的线程量满了,那么新被execute进来的线程,就会被存储进BlockQueue,BlockQueue的大小就是maximumPoolSize - corePoolSize的大小。

线程池和五种状态:

  • Running:能接受新的execute以及处理已添加的任务
  • Shutdown:不接收任何新的execute,可以处理已添加的任务
  • Stop:不接受任何新的execute,不处理已添加的任务
  • Tidying:所有任务已经终止,ctl记录的任务数量为0.
  • Termiated:线程池彻底终止,则线程池转换为Terminated状态。


那么这么多线程池状态和这么多线程的信息,是如何保存的呢?
这里线程池内部用到了一个32字节的Integer类型来记录线程池的状态和线程数量信息。



这个Integer类型的高3未二进制用来表示线程池的状态,后29为用来表示线程的数量。
线程池定义了这么几个数字作为线程的状态

RUNNING = -1
SHUTDOWN = 0
STOP = 1
TIDYING = 2
TERMINATED = 3

并且所有数字都想做移位29位。
《《 COUNT_BITS(COUNT_BITS=29)

最终会得到高三位为:
RUNNING = 111
SHUTDOWN = 000
STOP = 001
TIDYING = 010
TERMINATED = 011


他是怎么得到的呢?

我们来回顾一下基础

拿-1来举个例子
众所周知,
1在32位Integer的类型中二进制为:
0000 0000 0000 0000 0000 0000 0000 0001
那么-1就应该去1的反位并且再加上一个符号位1000
则,-1就应该为:
1000 1111 1111 1111 1111 1111 1111 1111
那么-1向左移位29位,低位补0,那么则
-1 《《 29 等于
1110 0000 0000 0000 0000 0000 0000 0000
所以高三位为111


所以这就是RUNNING的高三为为111的由来
后面的29位用于存储线程的数量。


这种应用基本数据高效存储的思想可以用于存储一些记录,有点就是不用去多个变量的读取,提升速度。


具体线程池的实现可以百度搜索JAVA线程池的实现,在这里只是浅谈一下线程池的好处以及浅层原理。

(本文完)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

Captcha Code