深入理解Java虚拟机笔记——Java内存模型与并发编程

 

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

 

如果一个变量在多个CPU中都存在缓存,那么就存在缓存一致性问题

2种解决方法

  1. 通过在总线加LOCk锁的方式

阻塞了其他CPU对其他部件访问,总线锁住期间,CPU无法访问内存,导致效率低下

  1. 通过缓存一致性协议

MESI协议

当CPU写数据时,如果发现操作的变量是共享变量,既在其他CPU中也存在该变量的副本,会发出信号通知其它CPU将该变量的缓存行置为无效状态,因此当其它CPU需要读取这个变量时,发现自己缓存中该变量的缓存行是无效的,那么它就会从内存重新读取

 

并发编程的三个概念

  1. 原子性

既一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  1. 可见性

当一个线程修改了共享变量的值,其它线程立即得知这个修改。

  1. 有序性

程序执行的顺序按照代码的先后顺序执行。(因为在JVM处理程序时会发生指令重排序)

指令重排序

处理器为了提高程序运行效率,可能对输入代码进行优化,它不保证程序中的各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

Java内存模型

规定所有的变量都是存在主存中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行性,而不能直接对主存进行操作。并且每个线程不能访问 其他线程的工作内存。

《深入理解Java虚拟机笔记——Java内存模型与并发编程》

主内存和工作内存之间的交互

  1. lock(锁定):作用于主内存的变量,它把一个变量标识位一条线程独占的状态。
  2. Unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的辩论词啊可以被其他线程锁定
  3. read(读取):作为主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. Load(载入):作用于工作内存的变量,它把read操作从工作内存中得到的变量值放入工作内存的变量副本中。
  5. Use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作
  6. Assign(赋值):作用于工作内存的变量 ,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. Store(存储):作用于主内存的变量,它把工作内存中的一个变量的 值传送到主内存中,以便随后的write操作使用。
  8. Write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

 

  1. 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子操作,即这些操作是不可被中断的,要么执行,要么不执行

  1. 可见性

Java提供了volatile关键字来保证可见性。当一个变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有线程需要被读取时,它会去内存中读取新值

  1. 有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java中可以通过volatile关键字来保证一定的有序性。另外可以通过synchronized和Lock来保证有序性。synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

Java模型具有一些先天的有序性,既不需要任何手段就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行顺序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序

Happens-before原则

  1. 程序次序原则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写到后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁Lock操作
  3. Volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得到操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于中断线程的代码被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生线程的终止检测。
  8. 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始(finalize()方法是Object中的方法,当垃圾回收器将要回收对象所占用内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它的finalize方法,让此对象处理它生前的最后事情)

Volatile关键字

保证操作变量的可见性和有序性,不保证操作变量的原子性

《深入理解Java虚拟机笔记——Java内存模型与并发编程》

这段代码本应该是20000,但是为什么比20000小呢??

因为num++这个指令它并不具有原子性,它是分为几步执行的,volatile能够保证拿到的时候是正确的数据,但是由于它只走了一步,别的线程把这个变量的值改了,它并不知道还是用的之前的数据,所以导致最后的结果比20000小

可见性:

A),在汇编层会对volatile修饰的关键字加Lock前缀

Lock前缀指令实际上是一个内存屏障,内存屏障会提供三个功能

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入内存
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效

  B),在修改变量的过程中

  1. 将修改变量的副本写入主内存
  2. 其他线程的 副本置为无效

C),读的时:先判断volatile关键字修饰的变量是否有效,有效直接读取,反之,则 到主内存获取最新值

有序性(能禁止指令重排序)

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯 定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也 不能把volatile变量后面的语句放到其前面执行。

 

 

 

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/qq_37937537/article/details/81945584
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞