『读书笔记』Java并发编程的艺术(JMM内存模型)

前言

本文并非按照书中目录所写,为自己读后总结,个人觉得这本书有着比较深的学习价值,在此致敬本书作者。

并发编程模型两个关键问题

并发编程需要着手解决原子性、有序性、可见性三个问题,这三个问题侧重在线程通信与线程同步上。针对于这两个问题,有两种机制来保证: 共享内存 | 消息传递。
共享内存屏蔽通信细节,但需要显式指定线程同步顺序;消息传递由程序员主动发送消息,显式执行线程通信,线程同步由于自带发送顺序,隐式进行。

\ 线程通信 线程同步 典型语言
共享内存 隐式 显式 Java
消息传递 显式 隐式 Go

原子性、有序性、可见性

原子性:操作不可分割。CPU层面保证基础指令的原子性,对于复杂原子指令,比如交换指令CMPXCHG,采用总线锁or缓存行锁来保证原子性。需要注意的是,32位操作系统不对64位数据写入保证原子性,比如long类型或者double类型变量写入。
有序性:涉及到的指令重排分三种,编译级指令重排(编译器优化)、指令级指令重排(CPU指令并行)、 内存系统指令重排(CPU读/写缓存区),单线程模型下,CPU与编译器不会对有间接依赖的指令重排序。
可见性:针对上述三种指令重排,而引发线程之间的内存可见性问题。

进一步充电 缓存一致性协议之MESI

Java内存模型的抽象结构

JMM定义了共享变量存储于主存之中,每个线程都有一个私有的本地内存,存储共享变量的副本。这里的本地内存是一个抽象的概念,并不真实存在,它涵盖了CPU高速缓存(L1,L2,L3)、写缓冲区、编译器优化等等。为了保证内存可见,Java编译器在生成指令序列的适当位置插入内存屏障。

JMM内存屏障

JMM把内存屏障指令分为4类,见下表。


JMM内存屏障指令

上面这四个内存屏障简单来说,Load用于读取装载数据,Store用于存储,会保证前面的装载or存储<优先于>后面的装载or存储

volatile内存语义

当写一个volatile变量时,JMM会把该线程的本地内存的共享变量值刷新到主存。
当读一个volatile变量时,JMM会把该线程的本地内存置为无效,从主存获取共享变量。

  • volatile写之前的操作不会被编译器重排序到volatile写之后。
  • volatile读之后的操作不会变编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读,不能重排序。

为了实现volatile内存语义,编译器生成字节码通过插入内存屏障来禁止重排序

  • 在每个volatile写前面插入StoreStore屏障,确保volatile写之前的数据刷新到主存,并且不会重排序到volatile写之后。
  • 在每个volatile写后面插入StoreLoad 屏障,确保volatile写与后续可能的volatile读/写操作重排序(这个开销昂贵)。
  • 在每个volatile读后面插入LoadLoad 屏障,确保volatile读不会与后续的普通读重排序。
  • 在每个volatile读后面插入LoadStore 屏障,确保volatile读不会与后续的普通写重排序。

比较有意思的是volatile写之后的StoreLoad屏障,JMM可以选择在每个volatile写之后或者volatile读之前插入StoreLoad屏障,但由于通常共享变量读多写少,JMM最终选择在volatile写之后插入StoreLoad屏障,来提供一定的性能提升。
上面内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意程序中都能保证volatile的正确语义。JMM针对不同平台不同代码,会省略部分内存屏障来做优化。

锁(ReentrantLock)的内存语义

  • 公平锁与非公平锁释放时,都要写volatile变量state。
  • 公平锁获取时,首先会读volatile变量。
  • 非公平锁获取时,首先CAS更新volatile变量。
    编译器会为CAS的交换指令CMPXCHG加入lock前缀,lock前缀同时具有volatile读与volatile写的内存语义。

总结来说:加锁具有和volatile读相同的内存语义,解锁具有和volatile写相同的内存语义。
并发包下的大部分锁,同步器都是基于AQS实现的,并发包的基石是volatile、synchronize、cas,JUC的包有个通用的实现模式:首先声明共享变量为volatile,然后使用CAS原子更新实现线程之间同步,同时配合CAS或volatile读写的内存语义来实现线程之间的通信。

final域重排序规则

  • 编译器会在final域写之后,构造函数返回之前插入StoreStore内存屏障,禁止final域的写重排序到构造函数之外。
  • 初次读包含final域的对象引用,再初次读final域,禁止重排序。这两个操作之间存在间接依赖,大多数处理器本身就不会重排序,但也有少部分的处理器允许间接依赖的关系进行重排序。
    final的语义保证了正确构建的对象不需要使用同步,其他线程都能看到正确的被初始化之后的值。
    以下为错误示例代码,final引用从构造函数溢出
/**
 * @author YuanChong
 * @create 2020-03-29 18:50
 * @desc final引用从构造函数溢出示例
 */
public class FinalExample {
    private final int data;

    private static FinalExample ref;
    
    private FinalExample(int data) {
        this.data = data;
        ref = this;
    }
    
    public static void instanceObject() {
        new FinalExample(1);
    }

    /**
     * 并发下,A线程执行instanceObject,B线程执行readFinal,B线程读到的可能是0也可能是1
     * @return
     */
    public static int readFinal() {
        return ref.data;
    }
}

JMM屏蔽内存模型细节

JMM提供了as-if-serial语义与happens-before原则保证程序的正确执行。
happens-before提供给程序员易于理解,简单易懂的并发下内存可见性保证。
as-if-serial语义保证了不管怎么重排序,单线程程序的执行结果不能被改变。
需要注意的是,这两种语义只是JMM对程序员的保证承诺,JMM只保证执行结果,但具体是否涉及重排序还要看编译器与处理器的优化。这是JMM在编译优化与简单易懂的内存模型之间的一个权衡结果。因此,happens-before更应该理解成生效可见于,他与执行顺序无关

  • 程序顺序原则:本线程的每个操作生效可见于后续发生的所有操作
  • 锁规则:当前线程解锁生效可见于后续其他线程的加锁
  • volatile规则:volatile写生效可见于后续对volatile的读
  • 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C
  • start规则:如果A线程执行Thread.start()启动线程B,A线程的Thread.start()生效可见于B线程的后续操作
  • join规则:如果A线程执行Thread.join(),B线程的任意操作生效可见于A从Thread.join()中返回

我们结合happens-before的几个原则,可以分析出线程同步代码是否有可见性问题
比如A线程执行Thread.start()启动B线程,A线程做的共享变量的修改生效可见于B线程,这是由顺序性规则,start规则,传递性规则同时推断出来的。

楼主之前也分析过锁的happens-before推断,详见从happen-before角度分析synchronized与lock的内存可见性问题

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

推荐阅读更多精彩内容