天天看点

Java内存模型Java内存模型什么是硬件和操作系统的访问差异处理器优化和指令重排并发编程的问题Java内存模型Java内存模型的实现

Java内存模型

Java内存模型简称JMM(Java Memory Model),Java线程之间的通信由JMM控制,JMM决定了一个线程对一个共享变量的写入何时对另一个线程可见。但是它不是一个真实存在的,而是一个抽象的概念,主要是为了屏蔽各种硬件和操作系统的访问差异,保证Java程序再各种平台下面对内存的访问都能够达到一致效果的规范及机制。

什么是硬件和操作系统的访问差异

JMM是为了屏蔽各种硬件和操作系统的访问差异,那到底什么是访问差异?这就要扯到计算机的内存模型。我们所有的程序最终都会转化成机器码,并交由CPU执行,CPU执行机器码指令的时候,免不了会产生数据,而计算机上的数据是存放在内存中的,也就是计算机的物理内存。

随着CPU的发展,内存的读写速度严重跟不上CPU的运算速度。这种访问速度的差异导致CPU可能要花很长时间获取或写入数据。既然内存成为了计算机处理的瓶颈,那么人们很自然的想到的一个好办法就是利用缓存技术。于是人们就在CPU和内存之间增加了高速缓存。

那么此时程序在运行过程中,会将需要的数据从主内存中复制一份到CPU的高速缓存中,那么CPU进行计算时就可以直接从它的高速缓存读取数据,当运算结束后再将高速缓存中的数据刷新到主内存。

高速缓存存储交互很好的解决了处理器与内存的速度矛盾,但是也带来了一个新的问题:缓存一致性。而在现在的多核处理器系统中,每个核都有自己的告诉缓存,但是他们又共享同一主内存。

Java内存模型Java内存模型什么是硬件和操作系统的访问差异处理器优化和指令重排并发编程的问题Java内存模型Java内存模型的实现

处理器优化和指令重排

CPU和主内存之间增加了缓存,在多线程场景下可能会存在缓存一致性问题。除此之外还有一种硬件问题也很严重,那就是处理器优化。处理器为了使内部运算单元能够尽量的被充分利用,可能会对输入的代码进行乱序处理。

除了很多处理器会对代码进行优化乱序处理,现在很多编程语言的编译器在执行程序时,为了提高性能,编译器也会有类似的优化。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:

Java内存模型Java内存模型什么是硬件和操作系统的访问差异处理器优化和指令重排并发编程的问题Java内存模型Java内存模型的实现

这些重排序可能会导致多线程出现可见性问题。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

并发编程的问题

并发编程的时候常见问题有原子性、可见性和有序性问题。其实这些常见问题都是人们抽象定义出来的,而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。

我们说的并发编程为了保证数据的安全性,需要满足一下特征:

原子性:一个操作中CPU不可以在中途暂停后再调度,即不会被终端操作。要么不执行完成,要么就执行完成。

可见性:当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。

有序性:程序执行顺序按照代码的先后顺序执行

其实缓存一致性问题就是可见性问题;处理器优化可以导致原子性问题;指令重排会导致有序性问题。

Java内存模型

在现代计算机的内存模型中,因为在CPU和主内存之间增加了高速缓存,那么就有可能跑在不同核的Java线程读到的缓存不一样,从而出现缓存一致性问题?不能说我写的代码运行在单核处理器下是OK的,但是跑在多核处理器下就有问题。最简单的做法我们就干脆废除处理器优化,废除CPU缓存,让CPU直接和主内存交互,这显然又太暴力的。比较好的做法就是你自己定义一套规则,你所有的代码都在这一套规则下运行无论是跑在单核处理器下还是多核,保证在各种平台下对内存的访问都是能够保证一致的一种规则。这其实就是java内存模型。

JMM有如下规定:

所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中用到的变量的主内存副本的拷贝,线程对变量的所有操作都必须再工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主内存之间进行数据同步。

而JMM的作用就是用于工作内存和主内存之间数据同步的过程,它规定了如何做数据同步以及什么时候做数据同步。

Java内存模型Java内存模型什么是硬件和操作系统的访问差异处理器优化和指令重排并发编程的问题Java内存模型Java内存模型的实现

从上图来看,线程A要想和线程B通信的话,必须经历以下2个步骤:

  1. 线程A把工作内存A中更新过的共享变量刷新到主内存
  2. 线程B到主内存中读取线程A之前已经更新过的变量

Java内存模型的实现

所以再来强调一下:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

为了解决这些问题,Java给我们提供了一系列的关键字,比如volatile、synchronize、final等。这些其实就是JMM模型下封装了底层实现后提供给程序员使用的一些关键字。

原子性

一个操作中CPU不可以在中途暂停后再调度,即不会被终端操作。要么不执行完成,要么就执行完成。

在Java中为了保证原子性,可以使用对应关键字就是synchronize。被synchronize修饰的方法和代码块,生成字节码指令的前后会增加两个字节码指令monitorenter和monitorexit。表示方法内部的操作必须以原子性执行

可见性

当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile保证了不同线程操作共享变量的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存的时候,另一个线程立即可以看到最新的值。

除了volatile,Java中的synchronize和final两个关键字也可以实现可见性。只不过实现方式不同。

有序性

有序性规定程序执行顺序按照代码的先后顺序执行。在Java中可以使用synchronize和volatile来保证多线程之间操作的有序性。

volatile关键字会禁止指令重排,同时编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。而synchronize关键字是保证同一时刻只允许一条线程操作。