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

一、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从工作内存中得到的值放入主内存的变量中

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)

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

轻量级线程与内核线程一对一的线程模型

轻量级进程的缺点

  • 各种线程操作(创建、析构及同步)都需要进行系统调用。代价较高,因为需要在用户态(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):线程已经结束执行

线程状态转换关系





03/31/2018

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容