Java内存模型与线程——《深入理解JVM》读书笔记

一、Java内存模型

Java内存模型(Java Memory Model,JMM)是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致内存访问效果。

JMM的主要目标是定义程序中各个变量(Variables)的访问规则(涉及到内存中的存取)。此处的变量包括实例字段静态字段构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,不存在竞争问题。

1. 主内存与工作内存

主内存(Main Memory):所有变量都存储在主内存。

工作内存(Working Memory):每条线程有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝(不一定是整个对象,可能是其中的某一字段)。

  • 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
  • 线程间变量值的传递均需要通过主内存来完成。
Java内存模型与线程——《深入理解JVM》读书笔记_第1张图片
线程、主内存与工作内存

2. 内存间交互操作

JMM中定义了八种主内存和工作内存之间的操作。JVM实现时需要保证每个都是原子的、不可再分的(double和long变量有例外)。

基本操作

操作 作用对象 效果
lock 主内存变量 标识为一条线程独占的状态。
unlock 主内存变量 释放一个被锁定的对象
read 主内存变量 读到工作内存中,供load使用
load 工作内存变量 read传来的变量值放入工作内存的变量副本中
use 工作内存变量 将变量值传递给执行引擎
assign 工作内存变量 把从执行引擎接收的值赋给工作内存的变量
store 工作内存变量 把变量值传到主内存,供write使用
write 主内存变量 store从工作内存中得到的值放入主内存的变量中

readloadstorewrite的顺序是固定的,但不要求连续执行。

规则

  • 不允许readloadstorewrite操作之一单独出现
  • 不允许线程丢弃其最近的assign操作,即工作内存中改变的变量必须被同步到主内存
  • 不允许线程无原因(没发生assign)地把数据从工作内存同步到主内存中
  • 新变量只能在主内存中“诞生”
  • 一个变量同一时刻只允许一条线程对其进行lock,但可以被同一线程lock多次(需要相同次数的unlock来解锁)
  • lock了一个变量后会清空工作内存中次变量的值,使用前需要重新loadassign
  • 不允许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中的变量做了一次storewrite操作。可让前面volatile变量的修改对其他CPU立即可见。

(2) 对volatile变量的特殊规则

假设T是一个线程,V和W是两个volatile变量,则在进行readloaduseassignstorewrite操作时需要满足如下规则:

  • 线程T对变量V的use动作可以认为是和T对V的readload动作相关联,必须连续一起出现。即每次使用V前都必须从主内存刷新最新的值。
  • 线程T对变量V的assign动作可认为是和T对V的storewrite动作相关联,必须连续一起出现。即工作内存中每次修改V后都要立刻同步回主内存中。
  • 同一线程中,若useassignV先于W,则readwriteV也要先于W。即保证变量不会被指令重排序优化。

4. 对long和double型变量的特殊规则

JMM要求八种基本操作具有原子性,但对64bit的数据类型(longdouble允许JVM将没有被volatile修饰的64bit数据的读写操作分成两次32bit的操作。即doublelong的非原子性协定(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)

程序一般不会直接去使用内核线程,而是用内核线程的一种高级接口,即轻量级进程。也就是通常意义上所讲的线程。

Java内存模型与线程——《深入理解JVM》读书笔记_第2张图片
轻量级线程与内核线程一对一的线程模型

轻量级进程的缺点

  • 各种线程操作(创建、析构及同步)都需要进行系统调用。代价较高,因为需要在用户态(User Mode)内核态(Kernel Mode)中来回切换。
  • 每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的

(2) 使用用户线程(User Thread,UT)

进程与用户线程1对N

对内核透明

不是内核线程就可以认为是广义上的用户线程。狭义上的用户线程是指完全建立在用户空间的线程库上,系统内核不能感知存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。可以是快速且低消耗的

缺点

没有系统内核的支援,所有线程操作都需要用户程序自己处理。

(3) 使用用户线程加轻量级进程

用户线程与轻量级进程之间N:M的关系

用户线程的操作依然在用户空间中,比较廉价,且支持大规模;同时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):线程已经结束执行

Java内存模型与线程——《深入理解JVM》读书笔记_第3张图片
线程状态转换关系





03/31/2018

你可能感兴趣的:(Java内存模型与线程——《深入理解JVM》读书笔记)