Java内存模型JMM

Contents

  1. 1. java内存模型
    1. 1.1. 什么是内存模型?
    2. 1.2. Java内存模型JMM?
      1. 1.2.1. 重排序
      2. 1.2.2. 写缓存区
      3. 1.2.3. 内存屏障
      4. 1.2.4. happens-before规则
      5. 1.2.5. JAVA语言层面上实现可见性:synchronized
      6. 1.2.6. JAVA语言层面上实现可见性:volatile
      7. 1.2.7. volatile与synchronized的比较
  2. 2. 参考资料

在并发编程中,有两个关键问题需要处理:线程之间如何通信以及线程之间如何同步。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。java的并发采用的是共享内存模型。本文主要介绍Java内存模型JMM以及其实现内存可见性的方式。
本文是在看了《Think in JAVA》、《JAVA核心技术卷》以及相关的一些博客后的总结笔记,纯属个人理解。若有错误,望指出。文末会贴出参考资料的链接。


java内存模型

什么是内存模型?

参考文章:http://ifeve.com/memory-model/
根据这篇译文以及参考资料总结:

多核系统中的处理器一般有一层或多层的缓存,这些缓存通过加速数据访问和降低共享内存在总线上的通信来提供CPU性能,的同时,也会导致内存可见性的问题。因此内存模型定义了一个充要条件:“让当前的处理器可以看到其他处理器写入到内存的数据”以及“其他处理器可以看到当前处理器写入到内存的数据”。

内存模型分为强内存模型和弱内存模型。
强内存模型(strong memory model):能够让所有的处理器在任何时候任何指定的内存地址上都可以看到完全相同的值。
弱内存模型(weaker memory model):需使用内存屏障来刷新本地处理器缓存并使本地处理器缓存无效。内存屏障通常在lock和unlock操作的时候完成。

Java内存模型JMM?

JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。JMM的原理如下图所示:


线程A对共享变量的修改要想被线程2及时看到,必须要经过2个步骤:
step1:把工作内存A中更新过的共享变量刷新到主内存中。
step2:强主内存中最新的共享变量的值更新到工作内存B中。
1.JMM对正确同步的多线程程序的内存一致性做了保证;
2.顺序一致性内存模型: 理论参考模型, 一个线程中的所有操作必须按照程序的顺序来执行, (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。
~~在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序
~~JMM不保证对64位的long型和double型变量的读/写操作具有原子性(解决方法:使用volatile变量),而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

重排序分三种类型:
1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。(编译器重排序)
2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。(处理器重排序)
3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。(处理器重排序)
重排序导致多线程程序出现内存可见性问题。
内存可见性:一个线程对共享变量值得修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
编译器和处理器在重排序的时候,会遵守数据依赖性,不会改变存在数据依赖性的两个操作的执行顺序。
as-if-serial语义:指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

写缓存区

现代的处理器使用写缓冲区来临时保存向内存写入的数据。

  1. 写缓冲区可以保证指令流水线持续运行,
  2. 它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
  3. 可以减少对内存总线的占用。 通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写。

写缓冲区仅仅对它所在的处理器可见

内存屏障

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM把内存屏障指令分为四类:

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。

happens-before规则

happens-before的概念阐述操作之间的内存可见性

  • 程序顺序规则:一个线程中的每个操作,happensbefore 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happensbefore 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happensbefore 于任意后续对这个volatile域的读。
  • 传递性:如果A happensbefore B,且B happensbefore C,那么A happensbefore C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

JAVA语言层面上实现可见性:synchronized

synchronized:解锁前:将共享变量的最新值刷新到主内存中;加锁时,清空工作内存共享变量的值,从主内存读取最新值。(加锁与解锁是同一把锁)
线程解锁前对共享变量的修改在下次加锁时对其他线程可见
(1)获得互斥锁
(2)清空工作内存
(3)从主内存拷贝变量的最新复本到工作内存
(4)执行代码
(5)将更改后的共享变量的值刷新到主内存
(6)释放互斥锁

锁是java并发编程中最重要的同步机制, 锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

  1. 当线程获取锁时,JMM把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。
  2. 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

具体锁相关的知识点归纳在java多线程 这篇博文中。

JAVA语言层面上实现可见性:volatile

volatile:通过加入内存屏障和禁止重排序优化来实现的。
volatile内存语义的实现

  1. 线程写volatile变量过程:
    (1)改变线程工作内存中volatile变量副本的值
    (2)将改变后的副本的值从工作内存刷新到主内存
  2. 线程读volatile变量的过程:
    (1)从主内存中读取最新值到线程的工作内存中
    (2)从工作内存中读取volatile变量的副本
    JMM针对编译器制定的volatile重排序规则表:
  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
  1. volatile写之前的操作不会被编译器重排序到volatile写之后。
  2. volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. volatile:通过加入内存屏障和禁止重排序优化来实现的。JMM采取保守策略:
  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障
    保守策略下,volatile写插入内存屏障后生成的指令序列示意图:

    保守策略下,volatile读插入内存屏障后生成的指令序列示意图:

volatile不能保证复合操作的原子性
解决方法:

  1. 使用Lock
  2. 使用synchronized
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public void increase(){
    //first method
    /*lock.lock();
    try {
    this.number++;
    } finally {
    lock.unlock();
    } */

    //second method 减少锁粒度
    /*synchronized (this) {
    this.number++;
    }*/

    this. number++;
    }

volatile适用场合

  1. 对变量的写入操作不依赖其当前值
  2. 该变量没有包含在具有其他变量的不变式中

volatile与synchronized的比较

  1. volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  2. 从内存可见性角度,volatile读相当于加锁,volatile写相当于解锁
  3. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  4. synchronized保证可见性和原子性;volatile保证可见性,不能保证原子性
  5. volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值;
  6. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
    当volatile能够保证线程安全时,尽量考虑volatile

参考资料

书本:
《Think in Java》
网址:
http://www.hollischuang.com/archives/1003
http://ifeve.com/memory-model/
http://ifeve.com/jmm-faq/
http://blog.csdn.net/zhangerqing/article/details/8214365