一、Java内存模型
Java内存模型(Java Memory Model,JMM)是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致内存访问效果。
JMM的主要目标是定义程序中各个变量(Variables)的访问规则(涉及到内存中的存取)。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,不存在竞争问题。
1. 主内存与工作内存
主内存(Main Memory):所有变量都存储在主内存。
工作内存(Working Memory):每条线程有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝(不一定是整个对象,可能是其中的某一字段)。
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
- 线程间变量值的传递均需要通过主内存来完成。
2. 内存间交互操作
JMM中定义了八种主内存和工作内存之间的操作。JVM实现时需要保证每个都是原子的、不可再分的(double和long变量有例外)。
基本操作
操作 | 作用对象 | 效果 |
---|---|---|
lock |
主内存变量 | 标识为一条线程独占的状态。 |
unlock |
主内存变量 | 释放一个被锁定的对象 |
read |
主内存变量 | 读到工作内存中,供load 使用 |
load |
工作内存变量 | 把read 传来的变量值放入工作内存的变量副本中 |
use |
工作内存变量 | 将变量值传递给执行引擎 |
assign |
工作内存变量 | 把从执行引擎接收的值赋给工作内存的变量 |
store |
工作内存变量 | 把变量值传到主内存,供write 使用 |
write |
主内存变量 | 把store 从工作内存中得到的值放入主内存的变量中 |
read
和load
,store
和write
的顺序是固定的,但不要求连续执行。
规则
- 不允许
read
和load
,store
和write
操作之一单独出现 - 不允许线程丢弃其最近的
assign
操作,即工作内存中改变的变量必须被同步到主内存 - 不允许线程无原因(没发生
assign
)地把数据从工作内存同步到主内存中 - 新变量只能在主内存中“诞生”
- 一个变量同一时刻只允许一条线程对其进行
lock
,但可以被同一线程lock
多次(需要相同次数的unlock
来解锁) -
lock
了一个变量后会清空工作内存中次变量的值,使用前需要重新load
或assign
- 不允许
unlock
未被lock
的变量,也不允许unlock
其他线程lock
的变量 -
unlock
一个变量前必须先将其同步回主内存
除此之外,也可通过先行发生原则判断一个访问在并发环境下是否安全。
3. 对volatile变量的特殊规则
(1) volatile变量的两种特性
a. 保证此变量对所有线程的可见性
一个线程修改了这个变量的值,新值对其他线程来说是可以立即得知的。
注意
不意味着volatile
变量对所有线程都是立即可见的。该变量也可以存在不一致的情况,但由于使用前需要刷新,执行引擎看不到不一致的情况,故可认为不存在一致性的问题。
volatile变量的运算是并发不安全的
若非原子操作运算,可能会出现并发下的问题,比如自增自减运算。
b. 禁止指令重排序优化
JMM中描述的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
有volatile
修饰的变量在赋值后多执行了一个lock addl $0x0, (%esp)
,相当于一个内存屏障(Memory Barrier或Memory Fence),指重排序时不能把后面的指令排到内存屏障之前的位置。该句中的addl $0x0, (%esp)
是个空操作(因为lock
前缀不允许使用空操作指令nop
)。
lock
前缀的作用是使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或别的内核无效化(Invalidate)其Cache。相当于对Cache中的变量做了一次store
和write
操作。可让前面volatile
变量的修改对其他CPU立即可见。
(2) 对volatile变量的特殊规则
假设T是一个线程,V和W是两个volatile
变量,则在进行read
、load
、use
、assign
、store
和write
操作时需要满足如下规则:
- 线程T对变量V的
use
动作可以认为是和T对V的read
和load
动作相关联,必须连续一起出现。即每次使用V前都必须从主内存刷新最新的值。 - 线程T对变量V的
assign
动作可认为是和T对V的store
和write
动作相关联,必须连续一起出现。即工作内存中每次修改V后都要立刻同步回主内存中。 - 同一线程中,若
use
或assign
V先于W,则read
或write
V也要先于W。即保证变量不会被指令重排序优化。
4. 对long和double型变量的特殊规则
JMM要求八种基本操作具有原子性,但对64bit的数据类型(long
和double
)允许JVM将没有被volatile
修饰的64bit数据的读写操作分成两次32bit的操作。即double
和long
的非原子性协定(Nonatomic Treatment of double and long Variables)。
故并发状态下,未声明为volatile
的这两类变量可能被某些线程读取到一个“半个变量”的数值。不过比较罕见,因为商用JVM几乎都选择将此实现为原子性操作。
5. 原子性、可见性与有序性
原子性(Atomicity)
- 八种基本操作
-
synchronized
关键字
可见性(Visibility)
-
volatile
变量 -
synchronized
关键字 -
final
关键字:被修饰的字段在构造器中一旦初始化完成且构造器没有把this
的引用传递出去(可能导致别的线程访问初始化了一半的对象),那该变量就能被其他线程无需同步地正确访问。
有序性(Ordering)
-
volatile
变量 -
synchronized
关键字
注意
sychronized
关键字虽然看起来万能,但代价是对性能的影响。
6. 先行发生(happens-before)原则
在之前学习Go的内存同步时接触过这个概念。该原则是判断数据是否存在竞争、线程是否安全的主要依据。
概念
先行发生是JMM定义的两个操作间的偏序关系。如果A先行发生于B,就是说A产生的影响能被B观察到。“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
确定的先行发生关系
以下先行发生关系无需任何同步器协助。若两操作间的关系不在此列,且无法从以下规则中推导出来的话,就没有顺序性保障,JVM可以随意进行重排序。
- 程序次序规则(Program Order Rule):同一线程内按程序代码(控制流)顺序,前面的先行发生于后面的。
-
管理锁定规则(Monitor Lock Rule):对同一个锁的
unlock
先行发生于时间上后面的lock
操作。 -
volatile
变量规则(Volatile Variable Rule):对一个volatile
变量的写操作先行发生于时间上后面对该变量的读操作。 -
线程启动规则(Thread Start Rule):
Thread
对象的start()
方法先于此线程的每一个动作。 -
线程终止规则(Thread Termination Rule):线程中所有操作都先行发生于此线程的终止检测,比如
Thread.join()
方法的结束、Thread.isAlive()
的返回值为false
等。 -
线程中断规则(Thread Interruption Rule):对
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,如Thread.interrupted()
方法。 -
对象终结规则(Finalizer Rule):对象的
构造函数执行结束
先行发生于finalize()
方法的开始。 - 传递性(Transitivity):若A先行发生于B,B先行发生于C,则A先行发生于C。
二、Java线程
1. 线程的实现
线程可以共享进程资源(内存地址,文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
(1) 使用内核线程(Kernel-Level Thread,KLT)
内核线程
- 直接由OS内核支持
- 由内核完成线程切换
- 内核通过调度器(Scheduler)进行调度,并负责将线程的任务映射到各个处理器上。
轻量级进程(Light Weight Process, LWP)
程序一般不会直接去使用内核线程,而是用内核线程的一种高级接口,即轻量级进程。也就是通常意义上所讲的线程。
轻量级进程的缺点
- 各种线程操作(创建、析构及同步)都需要进行系统调用。代价较高,因为需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
- 每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
(2) 使用用户线程(User Thread,UT)
对内核透明
不是内核线程就可以认为是广义上的用户线程。狭义上的用户线程是指完全建立在用户空间的线程库上,系统内核不能感知存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。可以是快速且低消耗的。
缺点
没有系统内核的支援,所有线程操作都需要用户程序自己处理。
(3) 使用用户线程加轻量级进程
用户线程的操作依然在用户空间中,比较廉价,且支持大规模;同时OS提供支持的轻量级进程作为用户线程和内核线程之间的桥梁,降低进程被完全阻塞的风险。
类似goroutine。
(4) Java线程的实现
JVM规范未限定,由JVM的实现决定。不过这些差异对Java程序的编码和运行是透明的。
2. Java线程调度
协同式调度
- 线程执行时间由线程本身控制,工作执行完后主动通知系统切换到另一线程
- 实现简单,没有线程同步问题
- 缺点:线程执行时间不可控,容易阻塞
抢占式调度(Java使用的)
- 系统分配执行时间(
Thread.yield()
可以让出执行时间,但没有可以获取的方法) - 不存在一个线程导致整个进程阻塞的问题
- 存在优先级控制,但不太靠谱
3. 状态转换
线程状态
新建(New):创建后未启动
可运行(Runable):包括OS线程中的Running和Ready,可能正在执行,可能等CPU分配执行时间
-
无限期等待(Waiting):不会被分配CPU执行时间,要等待被其他线程显式地唤醒。比如:
- 没有设置
Timeout
参数的Object.wait()
方法 - 没有设置
Timeout
参数的Thread.join()
方法 -
LockSupport.park()
方法
- 没有设置
-
限期等待(Timed Waiting):不会被CPU分配执行时间,不过无须等待其他线程唤醒,而是在一定时间后被系统自动唤醒。比如:
-
Thread.sleep()
方法 - 设置了
Timeout
参数的Object.wait()
方法 - 设置了
Timeout
参数的Thread.join()
方法 LockSupport.parkNanos()
LockSupport.parkUntil()
-
阻塞(Blocked):与“等待状态”的区别在于“阻塞状态”等待获取一个排他锁
结束(Terminated):线程已经结束执行
03/31/2018