「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」
计算机存在着三级的缓存,CPU也不是直接操作内存,而是把内存的数据读取到缓存中,但是内存的读写就会造成不一致。
Java虚拟机规范中视图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下达到一致的内存访问效果。
原则
三大特性:原子性,可见性和有序性。
并不真实存在,只是描述的一种约定或者规范。
屏蔽各个硬件平台和操作系统的内存访问差异。达到一致的内存访问结果。
sync符合原子性,可见性和有序性。三大特性。
但volatile只符合可见性和有序性。
可见性:
当一个线程修改了某一个共享变量的值,其他线程立马能够感知到该变动。
JMM规定了所有的变量都存储在主内存中。
Java中的普通的共享变量并不能保证可见性,因为数据修改被写入内存中的时间是不确定,多线程并发的条件先,很容易就会出现脏读,所以每一个线程就会有自己的工作内存,线程就会在自己的工作内存中,保存该线程使用的变量的主内存副本拷贝的变量值,所有的操作都必须在线程自己的工作内存中进行,不能够直接读取主内存中的值。
原子性: 操作是不可中断,多线程环境下,操作不能被其他线程干扰。
有序性:(指令重排)
默认情况下都是从上到下进行排序,但是为了程序的性能,编译器和处理器通常会对指令序列进行重新排序。
指令的排序可以保证串行的语义一致,但是没有义务保证多线程的语义也一致,即有可能产生脏读。
在单线程下,确保最终执行结果和代码顺序的执行结果一致。
处理器在进行重排序的时候必须要考虑指令之间的数据依赖性(数据从上一步的结果得来或者有源可找)。
多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
多线程对变量的读取过程
线程创建时,JVM都会创建一个工作内存(栈空间),java内存模型规定了所有的变量都存储在主内存中。
- 总结:
定义的所有的共享变量都存储在物理主内存中。
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
线程对共享变量所有的操作都必须现在线程中自己的工作内存中执行后写回主内存,不能直接读取主内存。
不同线程不能直接访问其他线程的工作内存的变量。
多线程的先行发生原则-----happens-before
(含有可见性和有序性的约束)
理解:JMM中,如果一个操作的执行结果需要对另一个操作可能性或者代码重排序,那么这两个操作之间就必须存在happens-before关系(需要先有哪一步,才能执行哪一步)。
该原则:可以判断数据是否存在竞争,线程是否安全的手段,可以解决并发环境两个操作之间是否存在可能冲突的所有问题,而不再需要陷入java内存模型的底层原理中。
-
总原则:
-
如果一个操作happens-before另一个操作,第一个操作的结果对于第二个操作是可见的,同时第一个操作需要在第二个操作之前。
-
两个操作之前存在happens-before关系,并不意味着一定要按照happens-before原则指定的的顺序来执行(允许重排序)。如果重排序之后的执行结果与按照happens-before关系的执行结果一致,那么这种重排序不违法。
例如:1+2+3 = 3+2+1,结果一致,不管是否重排序。
-
happens-before的8条原则:
1. 次序规则:前一个操作的结果可以被后续的操作获取。 2. 锁定规则:上一个线程unlock,下个线程才能获取锁,进行lock。 3. volatile变量规则:对于一个volatile变量的写操作先行于读操作,前面的写对于后面的读是可见的。 4. 传递规则:A先行于B,B先行于C,三条线程。 5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。 6. 线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 7. 线程终止规则:Thread::isAlive()的返回值等手段检测线程是否已经终止执行。 8. 对象终结原则:一个对象的初始化(构造函数执行结束)完成先行发生于它的finalize()方法的开始。 (对象没有完成初始化之前,是不能调用finalized方法的。)
案例: 假设存在线程A和B, 线程A先(时间上的先后)调用了setValue(1), 然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
答案是不一定。并不能确定结果。
解决办法:1. 在getter和setter方法加上sync关键字
2. 使用volatile定义变量。