JMM和底层实现原理

一、内存模型

Java内存模型(Java Memory Model,JMM)是一种规范,描述了Java虚拟机如何提供安全、正确地访问共享内存的机制它定义了Java程序中各个线程之间的数据交互方式,并规定了volatile关键字等多种同步机制的使用方式

在JMM中,每个线程拥有自己的本地内存(Local Memory),同时共享一个主内存(Main Memory)当一个线程执行操作时,它会将需要访问的变量从主内存复制到本地内存中进行操作,在操作完成后再将结果写回主内存。

1.1 JMM内存 模型的抽象架构

image.png
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:

  1. 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
  2. 线程 2 到主存中读取对应的共享变量的值。

也就是说,JMM 为共享变量提供了可见性的保障。

不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:

  1. 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
  2. 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。

1.2 重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。总的来说重排序分成两类:
**编译器优化的重排序。**编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
**处理器重排序。**现在处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

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

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

二、原子性、可见性、有序性

2.1 含义解释

原子性:是指一个操作不可中断,即使在多线程一起执行的情况下,一个操作一旦执行,就不会被其他线程打扰。
实现原理:MESI中,数据修改时,在回写到内存之前,会先锁住缓存行,并标记为修改状态,并想总线发送消息,其他处理器在操作之前,会检查该行是否lock,是不在 操作。这样就保证了原子性。
可见性:由于写缓存和无效队列带来了可见性问题,解决可见性问题则两个操作
flush: 当读操作时,强制将无效队列刷新到缓存区中
reflush:当写操作时,强制将写缓存区写入到内存去中
有序性:由于处理器1写到到缓存区中,因为不可见性,处理器2读取操作,读取旧的,任务读在写之前。通过加入内存屏障来保证

2.2 JMM对原子性问题的保证

**自带原子性保证:**在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。
**synchronized:**synchronized可以保证边界操作结果的原子性。synchronized可以防止多个线程并发的执行同一段代码,从结果上保证原子性。
**Lock锁:**Lock锁保证原子性的原理和synchronized类似。
**原子类操作:**JDK提供了很多原子操作类来保证操作的原子性,例如基础类型:AtomicXxx;引用类型AtomicReference等。原子类的底层是使用CAS机制,这个机制对原子性的保证和synchroinized有本质的区别。CAS机制保证了整个赋值操作是原子的不能被打断,二synchronized只能保证代码最终执行结果的正确性,也就是说,synchronized消除了原子性问题对代码最后执行结果的影响。

2.3 JMM对可见性问题的保证

在多线程环境下,一个线程对共享变量的修改,不仅要对本线程可见,而且要对其他线程可见。造成可见性的主要原因是由于CPU多核心和高速缓存(L1,L2,L3)。JMM对可见性问题,提供了如下保证:
**volatile:**使用volatile关键字修饰一个变量可以保证变量的可见性,大概的保证语义如下(详细的参看volatile的内存语义章节)

  • 线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
  • 线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。

**synchronized:**使用synchronized代码块或者synchronized方法也可以保证共享变量的可见性。当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监听器保护的临界区代码必须从主内存中读取共享变量,从而实现共享变量的可见性。
**Lock锁:**使用Lock相关实现类也可以保证共享变量的可见性。其原理同synchronized。
**原子操作类:**原子类底层使用的是CAS机制。java中CAS机制每次都会从主内存中获取最新值进行compare,比较一致之后才会将新值set到主内存中去。而且这个操作是一个原子操作,所以CAS每次操作每次拿到的都是主内存中的最新值,每次set的值也会立即写到主内存中。

2.4 JMM对有序性问题的保证

程序执行的顺序按照代码的先后顺序执行。在JMM允许的重排序环境下,单线程的执行结果和没有重排序的情况下保持一致。JMM中提供一下方式来保证有序性:
**happens-before原则:**happens-before原则是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。这里的“影响”包括修改共享变量,方法调用。详细的happens-before说明请参看happens-before原则章节。
**synchronized机制:**synchronized能够保证有序性是因为synchronized可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM能够保证代码的串行语义;虽然使用synchronized的代码块,还可以发生指令重排序,但是synchronized可以保证只有一个线程执行,所以最后的结果还是正确的。
**volatile机制:**volatile的底层是使用内存屏障(详细请参看内存屏障章节)来保障有序性的。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
多线程面临的两个问题线程之间的通信和线程之间的同步,这两个问题如果仔细分析,从结果的角度看线程之间的通信就是可见性问题,线程之间的同步就是原子性和有序性的问题。

总结JMM对特性提供的支持如下:

特性volatile关键字synchronized关键字Lock接口Atomic变量
原子性无法保障可以保障可以保障可以保障
可见性可以保障可以保障可以保障可以保障
有序性一定程度可以保障可以保障无法保障

三、happens-before原则

happens-before规则如下:
**程序顺序规则(Program Order Rule):**一个线程中的每个操作,happens-before于该线程中的任意后续操作。
**监视器锁规则(Monitor Lock Rule):**对一个锁的解锁,happens-before于随后对这个锁的加锁。
**volatile变量规则(Volatile Variable Rule):**对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
**start()规则(Thread Start Rule):**如果线程A执行线程B.start()(启动线程B),那么A线程的B.start()操作happens-before于线程B中的任意操作。
**join()规则(Thread Join Rule):**如果线程A执行线程B.join()并成功返回,那么线程B中的任意操作happens-before于线程A从B.join()操作成功返回。
**程序中断规则(Thread Interruption Rule):**对线程interrupt()的调用happens-before于被中断线程的interrupted()或者isInterrupted()。
**finalizer规则(Finalizer Rule):**一个对象构造函数的结束happens-before于该对象finalizer()的开始。
**传递性规则(Transitivity):**如果A happens-before B,且B happens-before C ,那么A happens-before C。

了解了happens-before原则,下面举例帮助理解:

private int value = 0;
public void setValue(int value){
    this.value = value;
}
public int getValue(){
    return value;
}

假设两个线程A和B,线程A先(在时间上先)调用了这个对象的setValue(1),接着线程B调用了getValue()方法,那么B的返回值是多少?
对照happens-before原则,上面的操作不满下面的条件:

  • 不是同一个线程,所以不涉及:程序顺序规则。
  • 不涉及同步,所以不涉及:监视器锁规则。
  • 没有volatile,所以不涉及:volatile变量规则。
  • 没有线程的启动和中断,所以不涉及:start()规则,join规则,程序中断规则。
  • 没有对象的创建和终结,所以不涉及:finalizer规则。
  • 更没有传递规则。

所以,一条规则都不满足,尽管线程A在时间上与线程B具有先后顺序,但是,却不满足happens-before原则,也就是有序性并不会保障,所以线程B获取到的数据是不安全的!!!这也反向说明了happens-before原则提到的关系和时间的先后顺序没有关系。

时间先后顺序与先行发生原则之间基本没有太大关系,所以我们衡量并发安全问题的时候不要收到时间顺序的干扰,一切必须以先行发生原则为准。只有真正满足了happens-before原则,才能保证安全。

四、总结

**Java 内存区域和 JMM 有何区别?**这是一个比较常见的问题,很多初学者非常容易搞混。 Java 内存区域和内存模型是完全不一样的两个东西

  • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
  • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值