12.3 内存模型

1. 主内存与工作内存

线程、主内存、工作内存之间的关系.png

Java线程之间通信的话,必须经过2个步骤

  • 1)线程A把本地内存中更新过的共享变量刷新到主内存中去
  • 2)线程B到主内存中去读取线程A之前更新过的共享变量

2.从源码到指令序列的重排序

  • 2.1:编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  • 2.2:指令级并行重排序:现在处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其指令的执行顺序。
  • 2.3:内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
    重排序.png

    这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器(2.1),JMM的编译器规则会禁止特定类型的编译器重排序。对于处理器的重排序(2.2和2.3),JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Fence)指令来禁止特定类型的处理器重排序。

3.as-if-serial 和 happens-before

\color{red}{3.1.单线程程序(as-if-serial)}语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变

PS:为了遵循该语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果

\color{red}{3.2.happens-before}语义规则:
  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这种锁的枷锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C, 那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
\color{red}{3.3.happens-before}的关系定义:
  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。\color{#4285f4}{PS:是JMM对程序员的承诺}
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种操作也是被允许的(即:不是按照程序员编写的代码顺序执行的),\color{#4285f4}{PS:这条定义是JMM对编译器和处理器重排序的约束原则}
\color{red}{3.4.总结}
  • as-if-serial语义的编译器、runtime和处理器为编写单线程程序员创建了一个幻境:单线程程序是按程序的顺序来执行的
  • happens-before语义给编写正确同步的多线程程序的程序员创造一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的

4.volatile的内存语义

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
    PS:参照缓存一致性协议:https://www.jianshu.com/p/690b0e4baf61

4.1.volatile内存语义的实现


重排序规则表.png
  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile之后
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之后。
  • 当第一个操作是volatile写时,第二个操作是volatile读时,不能重排序。
JMM采取保守策略,如下
  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

5.锁的内存语义

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出(在释放这个锁之前对共享变量所做修改的)消息
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
//锁释放-获取的示例代码
class MonitorExample {
  int a = 0;
  public synchronized void writer(){   //1
      a++;                                            //2
  }                                                      //3
  public synchronized void reader(){ //4 
      int i = a;                                      //5
      .......                                            //6
  }
}
1)根据程序次序规则,1 happens-before 2, 2 h-b 3,  4 h-b 5, 5 h-b 6
2)根据监视器锁规则, 3 h-b 4
3)根据h-b的传递性,  2 h-b 5

//未使用监视器
class MonitorExample {
  int a = 0;
  public void writer(){   //1
      a++;                                            //2
      Thread.sleep(10);  //睡眠10毫秒,等待reader()的执行
  }                                                      //3
  public void reader(){ //4 
      int i = a;                                      //5
      if(i == 0){                                           //6
        System.out.println("当前i值为" + i + ",当前主内存中a值" + a);
      }                                            
  }
  public static void main(String[] args){
    for(;;){
        MonitorExample me = new MonitorExample();
        Thread t1 = new Thread(() ->{
            me.writer();
        });
        Thread t2 = new Thread(() ->{
            me.reader();
        });
        
        t1.setPriority(10);//优先让writer()方法执行
        t1.start();
        t2.setPriority(1);
        t2.start();
    }
  }
}

//出现不可见问题:当前i值为0,当前主内存a值为1

6.final域的内存语义

6.1 final域的重排序规则
对于final域,编译器和处理器要遵守两个重排序规则

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
    6.2 final语义在处理器中的实现(以X86为例)
  • 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore屏障。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

synchronized和volatile最终都是使用lock执行

synchronzied使用lock cmpxchg
valatile使用lock add$空指令

CAS

最终实现:cmpxchg
多核情况下会多添加lock指令,即 lock cmpxchg

硬件层面(intel):

  • 确保对内存的读-改-写操作原子执行,带lock前缀的指令在执行期间会使用缓存锁定来保证指令执行的原子性,PS:锁住一个北桥信息(电信号,新版的处理器不采用锁总线的方式)
  • 禁止该指令,与之前和之后的读写指令重排序
  • 把写缓冲区中的所有数据刷新到内存中

参考书籍:Java并发编程的艺术(方腾飞)

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