4. 线程模型
1. 前置概念
为什么在说 jvm 时,要先讲一下 Java 的内存模型? 这个问题的关键在于,jvm 属于操作系统之上的应用,jvm 是通过调用操作系统对外提供的相关接口,来完成对计算机底层硬件计算资源的使用, 也就是说 jvm 使用计算机计算资源的方式是依赖于操作系统使用计算机计算资源的方式的。 因此要想了解 jvm 的整体架构,就必须要先了解计算机操作系统使用底层硬件计算资源的方式。 结合操作系统使用底层硬件计算资源的方式和计算机底层硬件系统架构,并结合“堆管存储,栈管运行”的特性,抽象总结出 JMM 的结构。 最后,基于 JMM 的内存结构,实现 JVM 运行时的基本架构,加上高级语言需要最终转化为机器语言的,最终才有了 JVM 的整体架构。
1.1. 并发和并行

并发 : 多个任务在单个处理器上运行,单个处理器通过划分时间片,让任务依次获取执行权限,看上去好像是多个任务同时发生一样,解决了不同任务执行过程中存在的阻塞问题,可以理解为任务阻塞时,就让出处理器的执行权限,目的是更大限度的压榨处理器的处理能力
并行: 同一./ch04-threadmode/image/1681524639011.png ./ch04-threadmode/image/1726197928105.png
1.2. 并发编程的发展
传统的计算机应用使用计算机的计算资源的方式,并没有因为计算机硬件的快速发展而改变,这就造成了计算机硬件的性能并没有得到显著提升。
因此人们迫切希望改变传统计算机使用计算机计算资源的方式,于是便出现了:
- 使用缓存。
- 对指令进行重排序。
- 分时复用计算资源。
基于上面的三个技术手段实现的编程过程,就可以成为并发编程,这也可以看做是并发编程的本质。
结果人们又发现使用这三项技术会导致并发编程的运算结果与我们实际预期的运算结果不一致。人们分析不一致的原因发现,实际上是因为,并发编程中有这样的需求:
- 并发编程中的不同线程需要通信;
- 并发编程中的不同线程的指令没有保证顺序;
这两个点是我们实现并发编程时一定会产生的两个需求,这是由计算机使用计算资源的方式决定的,不为人的意志转移而转移。
比如,我们要实现从 1 加到 1 亿的过程,传统的方式就是简单的从 1 加到 1 亿,而有了多个核心的计算机硬件时,我们就希望 core1 实现从 1 加到 1 万,core2 实现从 1 万加到 2 万……最后把多个核心运算的结果再合并起来。多个核心单独运算时并不需要保证核心之间的运算顺序,只需要保证单个核心内部的运算顺序的正确性即可(这个例子中,单个核心似乎也不需要保证运算顺序),这就说明并发编程中不同线程的指令的运算不需要保证顺序性。最后要把多个核心运算的结果合并起来时,就需要进行线程间的通信问题。 我们也可以认为,如果并发编程中不需要通信,也不需要同步时,并发编程的运算结果就与我们实际的运算结果是一致的。但这种场景在实际的生活中是很少见的,几乎不存在。
人们进一步追究,发现实际上是因为我们使用计算机计算资源的方式出了问题:
- 使用缓存产生了可见性问题。由于不同缓存之间访问速度的差异导致存储在不同缓存上的同一个变量,在不同线程访问这个变量时会读出不同的值;
- 对指令进行重排序产生了顺序性问题。编译器把高级语言编译成机器语言时,为了压榨计算机的性能,会对指令进行重新排序,重新排序后的指令会产生不同的运算结果,有些结果是我们所希望的,有些结果则不是,这就说明重排序后的指令在顺序上存在着对的顺序和错的顺序的问题。
- 分时复用计算资源产生了原子性问题。由于程序以单位时间片的方式占用 CPU,且长的指令执行时间长,短的指令执行时间短,因此这就涉及到指令长短的问题,也就是指令划分的问题。
人们通过各种复杂的技术手段解决了上面的三个问题后,发现并发编程的运算结果好像和我们预期的结果就一致了。于是我们就称并发编程安全了。由此引出并发编程三要素的概念:
- 原子性
- 可见性
- 顺序性
我们也说,遵循了并发编程三要素的程序是线程安全的。
在解决并发三要素问题过程中,人们根据并发编程需求的不同的实现思路,把并发编程模型分为两种:
- 基于共享内存的并发编程模型;
- 基于消息通信的并发编程模型;
1.3. 操作系统指令集
不同的计算机硬件就意味着需要使用不同的指令进行操作,例如 CPU 有 CPU 的操作指令、内存有内存的操作指令、硬盘有硬盘的操作指令……这些指令组合起来之后就是计算机的指令集。针对不同的计算机架构,计算机的指令集也有所不同。但计算机架构确定下来之后,指令集也就确定下来了。 计算机指令的组合使用过程,又需要有一个协调过程,这个协调过程就可以看作是操作系统来完成的。而我们要想完成并发编程就只能基于操作系统来完成。也就是说,编程语言使用并发编程的计算机资源的方式只能是基于操作系统对外提供的接口来实现。注意,这里所说的并发编程是指高级语言的并发编程。这就说明,Java 语言中的并发编程也是基于操作系统来实现的。
事实上,JDK 通过屏蔽不同的操作系统的指令集的差异,来实现统一的并发编程过程【这也是 Java 语言的跨平台的原因】。也就是说,不同的厂商根据不同的操作系统实现不同的 JDK,然后程序员又基于不同的 JDK 【但都遵循 Java 语言规范】来实现并发编程。由于 JDK 都遵循 jdk 规范,程序员编写的 Java 代码又都遵循 Java 语言规范,因此对外的表现是同一个并发编程的 Java 代码,在不同的平台上的运算结果是一样的。
2. Java 并发编程
Java 程序员编写遵循 Java 语言规范的并发编程代码,放到实现了统一的 JDK 规范的不同的 jdk 上运行,其运算结果是一样的。
在这个过程中,我们把 jdk 看作是调用操作系统接口的一个上层应用,我们编写的 Java 语言就是调用 jdk 接口的更上层次的应用。这样理解起来会更好理解一些。
2.1. JMM 模型
JMM 是基于上面一章中说的基于共享内存的并发编程模型而实现的。JMM 模型中要求,工作线程直接复用操作系统层面的线程。因此,我们可以认为 JMM 模型中没有对分时复用技术上的优化【也就是说分时复用技术上的优化是有操作系统负责的】
在整个并发编程模型中,除了有操作系统层面针对并发三要素问题的解决措施外,JDK 在调用操作系统的并发编程接口时也有不同的解决措施,但是无论怎样,都是基于 JMM 模型来完成的,加之 JMM 模型中要求工作线程直接复用操作系统层面的线程,因此解决措施也就可以分成两部分:
- 限制缓存的使用;
- 优化指令重排;
Java 中限制缓存的使用和优化指令重排都是由 JDK 帮我们完成了的,程序员只需要使用 Java 语言规范完成并发编程的代码,就能实现准确的并发编程。事实上 Java 中限制缓存的使用,以及优化指令重排的工作是极其复杂的,在这个过程中有编译器的参与,也有执行引擎的参与,甚至还有针对运行期内存结构的优化手段【如栈上分配、标量替换、同步消除等】的参与等,甚至为了便于程序员使用并发编程,还在这些复杂的实现过程之上又帮我们封装了一套易用的框架,这个框架就是 JUC。
3. 总结

// TODO./ch04-threadmode/image/1700364612715.png

栈管运行,堆管存储。
实现线程./ch04-threadmode/image/1700109303392.png
- 内核线程 (1:1线程模型)
- 用户线程 (1:N 线程模型)
- 用户线程 + 轻量级进程 混合 (N:M 线程模型)




虚拟机线程 周期任务线程 GC线程./ch04-threadmode/image/1700535441065.png 编译线程 信号调度线程
程序运行方式是通过创建线程的方式进行运行的,而线程的运行过程包括两大过程:内存分配和任务调度,Jvm中的线程调度多是由操作系统的调度来实现的,因此了解线程的内存分配即可。

[6] Monitor Ctrl-Break [5] Attach Listener [4] Signal Di./ch04-threadmode/image/1699847556605.png [3] Finalizer [2] Reference Handler [1] main
Attach Listener Attach Listener线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反 馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。
Signal Dispatcher 前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。
Finalizer 这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:
- 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;
- 该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;
- JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;
- JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难;
Reference Handler VM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
Monitor Ctrl-Break 这个线程我也不是很明白是干什么用的,oracle官网有详细信息,大家可以去看看


那问题来了,在linux下./ch04-threadmode/image/1700534369170.pngCtrl-Break,这就导致了,我们在等待所有子线程结束后的那句判断代码应该是>2而不是>1!!!
while (Thread.activeCoun./ch04-threadmode/image/1700533994216.png System.out.println(Thread.activeCount()); Thread.yield(); } 结论
windows下这个Monitor Ctrl-Break是不算在活动线程的,所以这样大于1是可以执行的,但是linux下应该是 大于2 ———————————————— 版权声明:本文为CSDN博主「chenxi004」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/chenxi004/article/details/104972979
