12.6 本章小结(P465)
概述
- 并发出现的根本原因在于计算速度与I/O速度的严重不符。
- 本章内容是Java开发者在实际工作中经常会接触的内容,因此对于本章节的学习不能不求甚解,一定要刨根揭底,力图理解每一个细节。
硬件的效率与一致性
- 处理器的计算速度与内存I/O的速度并不同步。
- 基于高速缓存的储存交互很好的解决了处理器与内存之间的矛盾。
- 高速缓存的引入会导致缓存一致性问题。
- 使用缓存一致性协议解决缓存一致性问题。
- 处理器可能会对指令进行乱序执行。
Java内存模型
- 一次编写,到处运行”是Java的核心优势。
- Java内存模型是实现跨平台性的基础之一。
- Java内存模型由主内存和工作内存组成,其作用是隔绝物理机内存模型实现的多样化,实现多线程对内存变量的安全共享。
- Java内存区域(堆栈模型)不是Java内存模型(JMM)的具体实现。
- Java内存模型(JMM)与Java内存区域(堆栈模型)并无关联,而是对内存两种不同维度的划分。
- Java内存模型共有八种交互操作,并且这些操作全都是原子操作。
- read - load / store - write实际上是传输 - 保存的过程,两者本质上是独立的,但逻辑上要求不可独立执行。
- lock - unlock在逻辑上也应该不允许独立执行,否则会造成死锁或无锁解锁的异常发生。
- 关键字volatile是Java最轻量级的同步机制,Java内存模型对volatile变量定义有三条特殊规则,并以此形成了两种特性。
规则1:只有当线程T对变量V执行的前一个操作为load时,线程T才可以对变量V执行use操作;
并且只有当线程T对变量V执行的后一个操作为use时,线程T才可以对变量V执行load操作。
- 该规则令read - load - use形成整体性的原子操作,使得volatile变量在被使用前会从主内存中读取最新值。
规则2:只有当线程T对变量V执行的前一个操作为assign时,线程T才可以对变量V执行store动作;
并且只有当线程T对变量V执行的后一个操作为store时,线程T才可以对变量V执行assign动作。
-
该规则令assign - store - write形成整体性的原子操作,使得volatile变量在被赋值后会立即同步回主内存。
-
规则1、2形成了volatile变量的第1个特性 ------ 保证可见性。可见性是指变量被一个线程修改后的新值是否可以被其它线程观察到。
-
volatile变量可保证可见性并不意味着其线程安全。
规则3:假定操作A是线程T对变量V实施的use或assign操作,操作F是和操作A关联的load或store操作,操作P是和操作F相关联的read或write操作;
假定操作B是线程T对变量W实施的use或assign操作,操作G是和操作B关联的load或store操作,操作Q是和操作G相关联的read或write操作;
如果A先于B,则P先于Q。
-
规则3形成了volatile变量的第2个特性 ------ 避免重排序。重排序是Java虚拟机的一种优化策略,在保证指令关联性(即执行结果)不变的情况下,将指令集打乱执行。
-
volatile变量可避免指令重排序。
-
Java内存模型只保证32位数据类型操作的原子性,对于64位数据类型(long和double)操作则由两个32位原子操作组成。
-
volatile关键字可以避免“半值”情况。
-
建议对共享的64位变量修饰volatile关键字。
-
Java内存模型三大特性:原子性、可见性与有序性。
-
Java内存模型通过read - road - use- assign - store - write这六个原子操作保证变量操作最小范围的原子性,更大范围的原子性则由lock - unlock实现。
-
需要提及的是,use与assign之间的变量计算操作属于执行引擎的职责范围,不归于内存模型,并且也不一定是(绝大部分都不是)原子操作,这也是volatile变量无法保证线程安全的根本原因。
-
普通变量无法保证可见性,但volatile变量可以,因为在use指令执行前会重新从主内存中读取最新值。
-
所谓的有序性,个人理解是在指定环境(指单线程或多线程环境)内,线程对安全的共享变量进行访问,而安全的共享变量,是指共享变量的值是“最新且正确”的。
-
如果在本线程内观察,所有操作都是有序的;如果在一个线程内观察另一个线程,所有操作都是无序的。
-
对于单线程环境,可以保证线程访问到的共享变量是安全。
-
对于多线程环境,无法保证某一线程访问到的共享变量是安全的,因为存在“重排序”与“工作内存与主内存同步延迟”现象。
-
先行发生原则(happens - before):如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到。
Java与线程
- 并发是一种概念,而多线程则是实现并发的具体方式。
- 内核线程实现是依赖操作系统的多线程实现方式。
- 用户线程实现与操作系统无关,是依赖用户空间线程库的多线程实现方式。
- 顾名思义,混合实现即是混合了内核线程实现及用户线程实现的多线程实现方式。
- 混合实现中,线程的本质是内核线程与用户线程的综合体(轻量级进程是两者沟通的桥梁)。因为一个用户线程可被多个内核线程支持,而一个内核线程也可支持多个用户线程,因此该实现也被称为N:M实现。
- 支持目前,Java线程实现采用内核线程实现(1:1实现)。
- 线程有协同式调度及抢占式调度两种调度方式,Java线程使用的是抢占式调度。
- 协同式调度实现简单,但稳定性极差。
- 抢占式调度由操作系统实现,稳定性强。
- 可以通过设置线程的优先级来建议操作系统优先执行该线程,这种建议并不是必然有效的,就类似于我们可以建议JVM进行GC,但JVM是否会执行是未知的。
- Java定义有6种线程状态。
- 无限期等待与限期等待的真正差别不在于等待时间。
- 无限期等待线程只能被另一个Java线程唤醒,而限期等待线程会被操作系统唤醒,想必已经有人猜到,这实际与线程的调度方式有所关系。
Java与协程
- 协程即用户线程,因为用户线程早期大多被设计成通过协同式对之进行调度而被简称为“协程”,但如今已与调度方式无关。协程分有栈和无栈两种,有栈协程就如同当前每个Java线程都附有一个虚拟机栈般附有一个调用栈(类似虚拟机栈,如果Java成功引入了协程,那就是虚拟机栈),而无栈协程自然就没有。
- 内核线程实现与如今“高并发”环境关系并不和谐。
- 内核线程是抢占式调度,调度成本十分高昂。
- Java研究者试图将协程作为突破口。
- 栈纠缠是一种模拟多线程的方式。
- 纤程是有栈协程的一种,更通俗的说是有栈用户线程的一种,是目前Java研究者试图引入协程的成果之一。
- 对于目前Java线程实现的尴尬处境,目前的主流解决方案思想是引入协程(用户线程),这种引入近似于内核线程实现(1:1实现)向混合实现(N:M实现)的转变。之所以说近似,是因为混合实现中协程是内核线程通过轻量级进程来进行管控的,也就是说协程依旧由操作系统来负责管理生命周期,即抢占式调度。但从12.5.2节中可知,Java研究者试图使协程保持一定的独立性,即不(完全)由操作系统来负责其生命周期,而是有其专属的管理方式,故只是近似而非等同。