JVM基础(4)- JMM:java内存模型与线程安全

通过了解JMM模型,可以了解多线程下出现安全问题的原因,并给出解决方案。

1. JMM模型基础

Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见

从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地缓存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。

在这里插入图片描述

本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

 

1.1. JMM下的线程通讯实例

线程A、B通讯需要两步:

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

在这里插入图片描述

从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。

 

1.2. Happens-Before 语义

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,涉及的相关方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

从 JDK5 开始,java 使用新的 JSR -133 内存模型。JSR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。

happens-before表示前面一个操作的结果对后续操作是可见的。

Happens-Before 规则

规则表述
1. 程序的顺序性规则
程序前面对某个变量的修改一定是对后续操作可见的。
2. volatile 变量规则
对一个 volatile 变量的写操作相对于后续对这个变量的读操作可见
3. 传递性
如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
4. 管程中锁的规则
锁的解锁 Happens-Before 于后续对这个锁的加锁,即解锁前的操作对后续加锁都是可加见的
5. 线程 start() 规则
指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程A在启动子线程 B 前的操作。 在这里插入图片描述
6. 线程 join() 规则
(A调用了子线程 B 的 join() )主线程 A 等待子线程 B 完成,当子线程 B 完成后,主线程能够看到子线程(对共享变量)的操作。
在这里插入图片描述
7. 线程中断规则
对线程interrupt()方法的调用 先发生于 被中断线程检测到中断事件的发生

Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

看一个栗子

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
      //如果在 1.5 以上的版本上运行,x 就是等于 42。
      //如果低于1.5 可能是0 可能是42
    }
  }
}

规则 1 : “x=42” 先于 “v=true” 操作;
规则 2 : 当“v=true”时,后续会读到v的值;
规则 3 : 如果线程B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 也是可见的。
在这里插入图片描述

 
 

2. volatile:可见性与防止指令重排

volatile 关键字最原始的意义就是禁用 CPU 缓存。

例如,我们声明 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

详见之前的文章

java并发关键字:volatile深入浅出:可见性、防止指令重排

 
 

3. 管程 与 线程同步

管程是一种通用的同步原语,synchronized 是 Java 里对管程的实现。

看一个栗子:

synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁

管程中锁的规则可以这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。

详见之前的文章
关键字: synchronized详解

 

4. final域 与 指令重排

对于基础类型

看一个栗子:

public class FinalDemo {
    private int a;  //普通域
    private final int b; //final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // 1. 写普通域
        b = 2; // 2. 写final域
    }

    public static void writer() {
        finalDemo = new FinalDemo();
    }

    public static void reader() {
        FinalDemo demo = finalDemo; // 3.读对象引用
        int a = demo.a;    //4.读普通域
        int b = demo.b;    //5.读final域
    }
}

看下final的规则:

禁止对final域的写重排序到构造函数之外,具体的:
编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

假设线程A执行writer,线程B执行reader:可能发生的情况是:

普通变量 a 可能“溢出”:(因为可能会被重排序到构造函数之外,此时线程B就有可能读到的是普通变量a初始化之前的值(零值))。
根据重排序规则,final域变量 b 不会被重排序,会在构造函数中赋值,这样线程B就能够读到初始化后的值。


对于引用类型

看一个栗子:

public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //操作1
        arrays[0] = 1;        //操作2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //操作3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }
}

final域的引用变量增加了一些约束:

在构造函数内对一个final修饰的对象的成员域写入(操作1,2),与随后在构造函数之外:把这个被构造的对象引用赋给一个引用变量(操作3),这两个操作是不能被重排序的。

假设:线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。


不可变的对象一定是线程安全的

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。

不可变的类型:

  • final 关键字修饰的基本数据类型
  • String
  • 枚举类型
  • Number 部分子类,如 Long 和 Double等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger和 AtomicLong 则是可变的。

参考:
https://pdai.tech/md/java/jvm/java-jvm-jmm.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值