【10】Java内存模型

As-If-Serial

  • 在单线程情况下,要给程序造成一个顺序执行的假象
    (即经过重排序的执行结果要与顺序执行的结果保持一致)
  • 如果两个操作之间存在数据依赖,那么即时编译器(和处理器)不能调整它们之间的顺序

happens-before

happens-before关系用来描述两个操作的内存可见性。如果操作X happens-before 操作Y,那么X的结果对于Y可见。

  • 线程内的happens-before
    在同一个线程中,字节码的先后顺序(program order)也暗含了happens-before关系
    在程序控制流路径中靠前的字节码happens-before靠后的字节码
    但不意味着前者一定在后者之前执行,实际上,如果后者没有数据依赖于前者,它们可能会被重排序

  • 线程间的happens-before

    • 解锁操作happens-before之后(时钟顺序)对同一把锁的加锁操作
    • volatile字段的写操作happens-before之后(时钟顺序)对同一字段的读操作
    • 线程的启动操作(Thread.start())happens-before该线程的第一个操作
    • 线程的最后一个操作happens-before它的终止事件
      其他线程通过Thread.isAlive()或者Thread.join()判断该线程是否终止
    • 线程对其他线程的中断操作happens-before被中断线程所收到的中断事件
      被中断线程的InterruptedException,
      第三个线程针对被中断线程的Thread.interrupted()或者thread.isInterrupted()调用
    • 构造器中的最后一个操作happens-before析构器的第一个操作

happens-before具有传递性

happens-before 问题剖析

int a=0, b=0;

public void method1() {
  int r2 = a;
  b = 1;
}

public void method2() {
  int r1 = b;
  a = 2;
}

默认线程内的happens-before关系
r2 赋值 happens-before b赋值
r1 赋值 happens-before a赋值

可能出现的结果:
单线程,method1->method2,(r1,r2)=(1,0)
单线程,method2->method1,(r1,r2)=(0,2)
多线程,没有重排序,A1->B1->A2->B2,(r1,r2)=(0,0)
多线程,重排序,A2->B1->B2->A1,(r1,r2)=(1,2)

对于出现(r1,r2)=(1,2)的情况做进一步剖析如下

拥有 happens-before 关系的两对赋值操作之间没有数据依赖,因此即时编译器、处理器都可能对其进行重排序。举例来说,只要将 b 的赋值操作排在 r2 的赋值操作之前,那么便可以按照赋值 b,赋值 r1,赋值 a,赋值 r2 的顺序得到(1,2)的结果。
Thread1      Thread2
  |            |
 b=1           |
  |          r1=b
  |           a=2
r2=a           | 

对于上述情况的解决方案

在程序中加入happens-before关系来解决,例如将a或b设置为volatile字段

private int a = 0;
private volatile int b = 0;

public void method1() {
    int r2 = a; // A1
    b = 1; // A2
}

public void method2() {
    int r1 = b; // B1
    a = 2; // B2
}

默认线程间的happens-before关系
A1 happens-before A2,B1 happens-before B2
A2 happens-before B1(`volatile字段的写操作happens-before之后(时钟顺序)对同一字段的读操作`)

所以依据happens-before的传递性:A1 happens-before B2
估 r2 不可能为2

上述问题的结论如下:
没有标记为volatile,在同一线程中,A1和A2存在happens-before关系,但没有数据依赖,因此可以重排序
一旦标记了volatile,即时编译器和CPU需要考虑多线程happens-before关系,因此就不能自由重排序了
解决类似问题的关键:构造一个跨线程的happens-before关系
操作X happens-before 操作Y,使得操作X之前的字节码结果对操作Y之后的字节码可见

Java 内存模型(JMM)的底层实现

  • JMM是通过内存屏障(memory barrier)来禁止即时编译器的重排序的

  • 对于即时编译器来说,会针对每个happens-before关系,向正在编译的目标方法中插入相应的内存屏障
    内存屏障类型:读读、读写、写读和写写
    这些内存屏障会限制即时编译器的重排序操作

  • 对于volatile字段,即时编译器将在volatile字段的读写操作前后各插入一些内存屏障

    1. 这些内存屏障不允许volatile字段写操作之前的内存访问被重排序至其之后
    2. 这些内存屏障不允许volatile字段读操作之后的内存访问被重排序至其之前

锁,volatile 字段,final 字段与安全发布

  • 锁(锁操作同样具备 happens-before 关系)
    解锁操作 happens-before 之后对同一把锁的加锁操作。如果编译器能够(通过逃逸分析)证明某把锁仅被同一线程持有,那它可以移除相应的加锁解锁操作。
    在解锁时,JVM同样需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见(类似于volatile)。

  • volatile字段

    • volatile字段是一种轻量级的,不保证原子性的同步,性能往往优于锁操作
    • 频繁地访问volatile字段也会因为不断地强制刷新缓存而严重影响程序的性能
    • 理想情况下,对volatile字段的使用应当是读多写少,并且应当只有一个线程进行写操作
    • volatile字段的每次访问均需要直接从内存中读写
  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值