Java内存模型-并发基础原理

543 阅读4分钟

并发编程基础原理,个人理解,如有错误请指正。推荐书籍《Java并发编程的艺术》

JMM (内存模型)

内存模型

  1. 每个线程都有一个私有的本地内存,本地内存中存储了该线程使用的共享变量的副本
  2. 本地内存只是JMM的一个抽象,具体实现依赖于具体硬件(高速缓存,寄存器等)
  3. 线程不直接读写主内存的共享变量,而是直接操作本地内存中的副本

硬件架构(常见架构,英特尔等)

结合JMM和硬件架构

  1. 当线程1抢占了核1的时间片,就拥有了核1及核1的L1 L2及共享L3使用权
  2. 在抢占的时间片内,线程1执行过程中若需要访问共享变量,就会去高速缓存中查找,若缓存miss则从主内存(也有可能从其他核的L1L2中查找)加载(此处包含预加载,为什么要有预加载见局部性原理)
  3. 通常所说的线程上下文切换过程中性能损耗是指第2步中缓存miss后的操作
  4. 若高速缓存中的副本被线程修改,cpu自行决定何时(一般是cpu空闲时)把修改后的值回写到主内存
  5. 若遇到Lock指令,cpu会强制回写主内存
  6. 步骤4,5中的回写主内存操作会使在其他缓存了该内存地址的数据(整个缓存行cacheLine)无效
  7. 当其他线程抢占任一核的时间片后,若高速缓存中的副本失效,会从主内存中重新加载共享变量
  8. 高速缓存和主内存一致性同步参考MESI一致性协议

volatile

顺序性

可见性

可见性定义

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

JVM如何实现volatile的可见性

  1. JVM解释执行或者编译执行时,若遇到共享变量的写操作会强制追加Lock指令
  2. 之后的过程见上面的步骤5,6,7

伪共享(并发编程的性能杀手)

伪共享描述

综合以上所述:缓存行容量为64字节(通常是,也有32的),缓存行中可以存放多个变量的副本(eg:8个long型),缓存行中任意一个变量被回写到主内存都会引起整个缓存行失效,无形中影响到了其他无竞争关系线程读取其他变量副本的成本。 若这些变量中有一个被volatile修饰,那么该变量的写操作必定触发回写主内存,必定会使其他变量的副本失效, 所以volatile要慎用

如何解决

  • padding
  public class Test {

      private volatile Long id;

      /**
        * 填充字段
        */
      private Long p0, p1, p2, p3, p4, p5;

      /**
        * 阻止Jvm的无用字段消除优化
        */
      public Long preventToBeEliminated() {
          return p0 + p1 + p2 + p3 + p4 + p5;
      }
  }
  • @sun.misc.Contended

    官方文档说明

    该注解实际上也是填充,只不过比方法1更智能但只适用于java8及以上

    –XX:+PrintFieldLayout -XX:-RestrictContended

  public class Test {


      private String age;
      private String name;

      /**
        * Contended注解可以将id移动到远离其他字段的地方
        */
      @Contended
      private volatile Long id;

  }

增加注解后的字段布局

局部性原理(扩展)

  1. 时间局部性
  2. 空间局部性
  3. 分支局部性
  4. 等等

空间局部性

如果某个位置的信息被访问,那和它相邻的信息也很有可能被访问到。 这个很好理解,程序代码中有很多循环遍历数组等操作。 不仅限于数组,线性(分布是连续的)数据结构都可以,比如Java对象的各个字段在堆中就是连续的内存块分布(详见Java对象的内存布局)

空间局部性的应用

  • 主内存缓存硬盘 page cache :kafka,mysql等

    mysql某个索引(索引在b+树叶子节点也是线性分布的)被命中后会一同加载该索引前后多条记录到同一个pageCache中, 这样可以减少磁盘IO次数,提升查询性能。kafka的消息log在磁盘中也是按分区内的消息顺序追加的,也可以很好的利用该特性

  • cpu高速缓存主内存 cache line:redis等

    redis中的数据结构散列集Hash,当key数量小于一定值时散列集会被压缩成数组就是为了利用cpu高速缓存,这也是为什么使用Hash比使用多个KV要快的原因。详见redis官网-内存优化

MESI缓存一致性协议(扩展)

缓存一致性协议MESI

  • M(修改,Modified)

    本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有)

  • E(专有,Exclusive)

    缓存行内容和内存中的一样,而且其它处理器都没有这行数据

  • S(共享,Shared)

    缓存行内容和内存中的一样,有可能其它处理器也存在此缓存行x拷贝

  • I(无效,Invalid)

    缓存行失效, 不能使用

Jvm堆中对象的内存布局(扩展)