Java 线程池理解
线程是调度 CPU 的最小单元,也叫轻量级进程 LWP(Light Weight Process)
两种线程模型:
- 用户级线程(ULT)
用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对 ULT 无感知,线程阻塞则进程(包括它的所有线程)阻塞。
- 内核级线程(KLT)
系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比进程操作快。
Java 虚拟机中使用的是哪一种线程模型?
如果 JVM 使用的是用户级线程,创建的线程不由操作系统管理,也就意味着内核对用户级线程ULT 是无感知的。那么当我们运行上述代码创建 300 个线程的时候,此时的操作系统是看不到有明显的线程数量变化。反之,如果 JVM 用的是 KLT,那么当我们创建 300 个线程的时候,此时的操作系统应该是可以观测到线程数量的变化。
在 Java 的 JS133 规范里面,并没有严格的说明使用的是 KLT 还是 ULT,但是,目前市面上绝大多数 JVM 虚拟机都是使用 KLT 线程模型。
操作系统在运行时将内存条格式化成一个一个的逻辑地址,每一个逻辑地址都对应着硬件地址。我们操作的时候其实都是通过操作系统去分配逻辑空间,操作系统把整个的运行空间大致划分成两块区域,一个是用户空间,另一个是内核空间。这两个空间的区别在于两者的权限级别不一样。权限设计的本质目的是防止无关人员越级去操作一些比较敏感的业务。
例如 WPS等应用软件是运行在用户空间的。硬件的使用通常是需要驱动程序的辅助才可以操纵硬件,所以一般硬件设备都是由操作系统去完成的。JVM 为什么可以创建线程,为什么可以调度 CPU 呢?它自己并没有驱动,所以它不可能调用底层的硬件,它必须要通过操作系统开放的 API来完成相应的操作。Linux 操作系统提供了函数库 p_thread 来操作线程。所以 JVM 创建线程就是通过调用内核系统开放的 API(p_thread)来创建线程,这个线程再映射到底层 CPU。
Java 的线程为什么会涉及到线程的状态切换?
因为Java线程运行在用户态,而线程使用的又是操作系统提供的接口。操作系统提供的接口,Java 线程可以使用,但是要将 Java 线程的权限提升。此时,会将 Java 线程的用户态陷入到内核态,从而取得内核的运行权限,才能够真正创建线程。此时创建的线程一般会放入到内核里面,内核操作系统会有一个线程栈表。用户APP里面创建的所有线程都会放入到线程表里面,由内核统一的进行维护和调度。
线程使我们程序运行的载体
线程为什么需要池化?
直接原因就是线程的创建过程非常麻烦,状态的切换是一个比较重的操作。线程的创建和销毁都是耗费资源的操作,所以我们希望能够尽可能的重用线程,这也是我们使用线程池的意义和目的。
线程池的意义
线程是稀缺资源,它的创建与销毁是一个相对偏重且消耗资源的操作,而 Java 线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。
什么时候使用线程池?
- 单个任务处理时间比较短(轻量级任务)
- 需要处理的任务数量很大
线程池的优点
- 重用存在的线程,减少线程创建,消亡的开销,提高性能。
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性,可统一分配,调优和监控。
ThreadPoolExecutor:常用的线程池
ForkJoinPool :专门用来解决计算密集型的任务
ScheduledThreadPoolExecutor:延时类的线程池
阻塞队列:在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作。(FIFO)意味着他是线程安全的队列。
阻塞队列分为有界队列和无界队列:
- 有界队列:队列的容量有大小限制。
- 无界队列:队列的容量无大小限制,但是也并不是意味着无界队列可以无穷大,无界队列只是理论上是无界的,但是实际上会受限于物理主机的内存大小。
假设有界队列只能存放 5 个元素,当队列存满了之后会怎么办?队列满,只能进行出队操作,所有入队的操作必须等待,也就是被阻塞。反之,队列空,只能进行入队操作,所有出队操作必须等待,也就是被阻塞。