Java内存模型

Java Memory Model, 用来屏蔽掉各种硬件和操作系统的内存访问差异, 以实现让java程序在各种平台下都能达到一致的并发效果

  • java内存模型的主要目标是定义程序中各个变量的访问规则, 即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
  • 此处的变量包括实例字段, 静态字段和构成数组的元素, 但是不包括局部变量和方法参数, 因为后者是线程私有的, 不会被共享, 继而也不会存在竞争问题

主内存与工作内存

java内存模型规定了所有的变量都存储在主内存中, 每条线程还有自己的工作内存, 线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝, 线程对变量的所有操作都必须在工作内存中进行, 而不能直接读写主内存中的变量; 不同线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成
image.png

内存间交互操作

主内存与工作内存间的交互协议:
lock(锁定): 作用于主内存的变量, 把一个变量标识为一条线程独占的状态
unlock(解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定
read(读取): 作用于主内存的变量, 把一个变量的值从主内存传输到线程的工作内存中
load(载入): 作用于工作内存的变量, 它把read操作从主内存得到的变量值放入工作内存的变量副本中
use(使用): 作用于工作内存的变量, 它把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
assign(赋值): 作用于工作内存的变量, 它把一个从执行引擎接收到的值赋值给工作内存的变量
store(存储): 作用于工作内存的变量, 它把工作内存中一个变量的值传递到主内存中
write(写入): 作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中

使用volatile型变量的特殊规则

当一个变量被定义成volatile之后, 它将具备两种特性:

  • 保证此变量对所有线程的可见性
  • 禁止指令重排序优化

原子性, 可见性与有序性

  • 原子性(Atomicity)

由java内存模型来直接保证的原子性操作包括 read, load, assign, use, store, write , 因此基本类型的数据的访问读写是原子性的, 此外synchronized块之间的操作也是原子性的

  • 可见性(Visibility)

可见性是当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改;
volatile变量通过”保证新值能够立即同步到主内存, 以及每次使用前从主内存刷新”, 而具有可见性
synchronized同步块通过”对一个变量执行unlock操作之前, 必须将变量同步回主内存中”, 而具有可见性
final关键字通过”被final修饰的字段在构造器中一旦被初始化完成, 并且构造器没有把this引用传递出去, 那么在其他线程中就能看到final字段的值”, 而具有可见性

  • 有序性(Odering)

如果在本线程内观察, 所有的操作都是有序的, 如果在一个线程中观察另一个线程, 所有的操作都是无序的
前半句是指”在线程内表现为串行的语义”, 后半句是指”指令重排序现象”和”工作内存与主内存同步延迟现象”

volatile关键字本身就包含了禁止指令重排序的语义
synchronized则是由”一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的有序性

先行发生原则

先行发生是java内存模型中定义的两项操作之间的偏序关系, 如果说操作A先行发生于B, 其实就是在说发生操作B之前, 操作A产生的影响能被操作B观察到, “影响”包括修改了共享内存中共享变量的值, 发送了消息, 调用了方法等

衡量并发安全问题时不要考虑时间顺序的干扰, 一切必须以先行发生原则为准

java内存模型中天然的先行发生关系

  • 程序次序规则: 程序的控制流顺序
  • 管程锁定规则: unlock操作先于同一个所的lock
  • volatile变量规则: 对volatile变量的写先于对这个变量的读
  • 线程启动规则: Thread对象的start()方法先于此线程的每一个动作
  • 线程终止规则: 线程中的所有操作都先行发生于对此线程的终止检测
  • 线程终端规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则: 一个对象的初始化完成先行发生于它的finalize()方法的开始
  • 传递性: A先行于B, B先行于C, 因此A先行于C

Java与线程

线程的实现

对Sun JDK来说, 在Windows/Linux版都是使用一对一的线程模型来实现的(第一种).
1.使用内核线程实现
程序(Procedure)使用内核线程(Kernel Thread, KLT)提供的一种高级接口-轻量级进程(Light Weight Process, LWP), 每个轻量级进程都由一个内核线程支持; 内核线程由操作系统内核支持.
image.png
优点: 由于内核线程的支持, 每个轻量级进程都是一个独立的调度单元, 即使有一个阻塞了, 也不会影响整个进程继续工作
缺点:

  • 由于基于内核线程实现, 因此各种进程操作都要进行操作系统调用, 而系统调用的代码相对较高, 需要在用户态和内核态中来回切换
  • 轻量级进程也要消耗一定的内核资源, 因此一个系统支持轻量级进程的数量是有限的

2.使用用户线程实现
狭义上的用户线程是指完全建立在用户空间的线程库上, 系统内核不能感知到线程存在的实现, 用户线程的建立, 同步, 销毁和调度完全在用户态完成, 不需要内核的帮助
image.png

优点: 不需要系统内核支援, 操作可以是快且低消耗的, 也可以支持更大规模的线程数量
缺点: 所有操作都需要用户程序自己处理, 诸如”阻塞如何处理”, “多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将异常困难, 甚至不可能完成

3.混合实现
image.png

Java线程调度

线程调度是线程分配处理器使用权的过程, java使用的是抢占式调度

  • 协同式调度

线程的执行时间由线程本身来控制, 线程执行完成主动通知系统切换另一线程
优点: 实现简单, 切换操作可知
缺点: 线程执行时间不可控制, 若一个线程有问题, 一直不告知系统进行切换, 程序会一直阻塞

  • 抢占式调度

每个线程由系统来分配执行时间, 线程分配不由线程本身来决定
优点: 不会由一个线程导致整个进程堵塞

状态切换

java定义了5种线程状态, 在任意一个时间点, 一个线程有且只能有其中的一个状态
新建, 运行, 无限期等待, 限期等待, 阻塞, 结束
image.png