Java并发-Java内存模型(JMM)

先来说说什么是内存模型吧

在硬件中,由于CPU的速度高于内存,所以对于数据读写来说会出现瓶颈,无法充分利用CPU的速度,因此在二者之间加入了一个缓冲设备,高速缓冲寄存器,通过它来实现内存与CPU的数据交互。我们现在的计算机都是多CPU多核的,而每个CPU都需要配备一个寄存器,那么问题来了,如果一个CPU对数据进行修改写入了寄存器但没及时更新到主存,另一个CPU也对其进行了修改,便会发生数据错误,最终得到的结果并非我们想要的。

如何解决这一问题呢?缓存一致性!我们需要一个缓存一致性模型来规范化我们的内存读写和寄存器读写,这样才能保证数据一致性。

因此:内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的CPU 有不同的内存模型。


那么什么是Java内存模型?

我们都知道Java的口号是write once, run anywhere。JMM便是实现这句话的最重要一步!

JVM通过定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

也就是说,JMM规范了线程的工作内存和主存之间的读写操作。Java 内存模型定义了两个重要的东西,1.主内存,2.工作内存。每个线程的工作内存都是独立的,线程操作数据只能在工作内存中计算,然后刷入到主存。这是 Java 内存模型定义的线程基本工作方式


JMM规范了三种重要的特征,这也是Java并发的基础:原子性,可见性,有序性。

原子性

这里的原子性和事务很相似,对于一个操作是不可以打断的,不可分割的,如果这个操作没有执行完,其他线程不可以干扰。

Java中的基本数据类型的操作大致可以理解为原子操作(32位下的long和double操作除外)。因为 java 虚拟机规范中,对 long 和 double 的操作没有强制定义要原子性的,但是强烈建议使用原子性的。因此,大部分商用的虚拟机基本都实现了原子性。

JMM定义了8中原子性操作:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

1、不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。

2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。

8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)

看到一张图很好的描述了这几个关键词的作用:

Java并发-Java内存模型(JMM)

这些操作JVM是不开放给程序员的,对于编程来说,我们只能使用monitorenter和monitorexit两个字节码指令,也就是 synchronized 关键字,因此在 synchronized 块之间的操作都是原子性的(加锁之后同时只能有一个线程通执行代码块,当然具有原子性了)。


 可见性

可见性是指一个线程对一个共享变量做出了修改,其余线程立刻可以得知这个修改。volatile,synchronized,final均可提供可见性保证

volatile是通过在指令后加上lock指令并与数字0求和,从而立刻将值写回主存,并令其余缓存行失效;

对于synchronizedJMM做了两条规定:

  1)线程解锁前,必须把共享变量的最新值刷新到主内存中

  2)线程加锁时,将清空工作内存*享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

   (注意:加锁与解锁需要是同一把锁)


 有序性

CPU、JVM都会对指令重排序,从而达到优化程序的目的,这种指令顺序调整在单线程里是感知不到的,不过在多线程程序中,这种调整会导致一些问题。JMM通过内存屏障来保证不会对一些必要的操作重排序

我们主要通过两个关键字来保证多个线程之间操作的有序性,volatile关键字本身就包含了禁止重排序的语义,而 synchronized 则是由 “一个变量同一时刻只允许一条线程对其进行加锁操作”这个规则获得的

人为控制有序性意味着JIT不能优化代码,因此要谨慎使用


总之:volatile 保证了可见性和有序性,synchronized 则3个特性都保证了,不过要根据自己业务的并发特性来判断synchronized的性能所带来的问题。

他们的功能有交集,什么时候用 volatile?

运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值


JMM的HappenBefore原则

刚才说到有序性可以通过 volatile 和 synchronized 来实现,但是我们不可能所有的代码都靠这两个关键字。实际上,Java 语言已对重排序或者说有序性做了规定,这些规定在虚拟机优化的时候是不能违背的。
1. 程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先发生于书写在后面的操作。
2. volatile 规则:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性。
3. 锁规则:解锁(unlock) 必然发生在随后的加锁(lock)前。
4. 传递性:A先于B,B先于C,那么A必然先于C。
5. 线程的 start 方法先于他的每一个动作。
6. 线程的所有操作先于线程的终结。
7. 线程的中断(interrupt())先于被中断的代码。
8. 对象的构造函数,结束先于 finalize 方法

上一篇:SVM分类器实现实例


下一篇:Java多线程时内存模型