内存模型
硬件的内存模型
在讲java内存模型之前,先来看看硬件的内存模型
CPU的处理速度和内存的读写不是一个数量级的,所以在CPU和主存之间加上了一层缓存
这种结构在单CPU的时候,处理的很好
但是当多CPU的时候,
这时候就会出现缓存一致性问题
当CPUA读取主存中的数据之后,对其进行修改,在将其刷新回主存的之前,CPUB读取主存中的数据,对其进行修改,将其刷新回主存,这时候CPUA也同时将其修改后的数据刷新回主存,那么这个数据到底以哪一个为准,这个就是缓存一致性问题
针对这个问题,就出现了缓存一致性协议
1、窥探性
2、目录型
有了这一层协议,在硬件层面,就解决了缓存一致性问题,即汇编语言能够运行在一个具有缓存一致性的内存视图中
Java的内存模型
设计编程语言的内存模型是为了能够该语言也可以拥有一个内存一致性的视图,于是在硬件内存模型之上,就有了高级语言的内存模型
Java内存模型就屏蔽了各种硬件的操作系统的内存差异,使得java可以正常的运行在各大操作系统上
虚拟机栈也可以叫做java方法栈,该栈中存放8大基础类型的数据和对象的引用
堆中存放着所有的java对象
内存读写指令
作用于主存 | 作用于工作内存 |
---|---|
lock:锁定 | load:加载数据 |
unlock:解锁 | store:存储数据 |
read:读取 | use:使用数据 |
write:写入 | assign:赋值 |
上面的图只是一种理想状态,会出现以下两种问题
1、可见性
当线程A将本地内存中的数据修改后,刷新回主存后,线程B直接使用本地内存中的数据,没有使用刷新后的数据,这就是可见性问题
2、原子性
当线程A修改了数据,还没刷新回主存,线程B也修改了数据,也要刷新回主存,那么这时候主存中应该刷新成哪个线程修改后的值呢
线程通信之间的同步问题,当多个线程在并发操作同一个数据的时候,会引发很多的问题,这些问题被总结为并发三要素
1、可见性
2、原子性
3、有序性
可见性
当一个线程修改了共享变量的值之后,其他所有使用该变量的线程都应该立刻得知此修改
两层含义
第一种含义
线程A修改了数据X,线程B需要使用到最新的数据X(这是线程B没有重新读取主存导致的)
1 | public class Demo { |
控制台不会返回响应码,而是一直死循环。
当thread1
开始循环的时候,本地内存中a=1
,当thread2
修改了a为0的时候,thread1
并不知道,而是一直使用着a=1
,所以会一直循环
两种解决方法
1、将a
变量加一个修饰词volatile
如果一个共享变量被volatile
修饰,那么该共享变量被修改后,将会直接写入主存,当其他线程读取该共享变量的时候,也会直接从主存中读取
2、使用synchronized
包裹,并使用该数据
synchronized
块中读写变量会隐式调用lock
和unlock
指令
1 | public class Demo { |
第二层含义
线程B需要读取到线程A修改后的数据x,但是因为指令重排,在线程A未修改数据x之前,线程B读到了数据x
1 | public class Demo2 { |
在硬件内存模型的时候就说过,在底层会存在指令重排的情况,
我们觉得的顺序应该是
1->2->3->4
但是在编译后顺序有可能就变成了
2->3->4->1
这也是一种可见性的问题
同样的这里我们也可以使用上述两种方法来解决这种问题
volatile
是禁止了当前变量与之前的代码语句进行指令重排
synchronized
就是将两段代码分别捆绑在一起,那么无论在thread1
中怎么指令重排,都不会影响thread2
对于变量的读取
Happens-Before原则
我们平时很少遇到可见性问题,因为我们站在了前人的肩膀上,设计内存模型的前辈已经帮我们解决了此问题,这就是Happens-Before原则
定义:对于两个操作A和操作B,这两个操作可以在不同的线程中执行,如果A Happens-Before B(即A先于B执行),那么可以保证当A操作执行完后,A操作的执行结果对B操作是可见的
- 程序顺序原则
- 锁定原则
- volatile原则
- 线程启动原则
- 线程结束原则
- 中断规则
- 终结器规则
- 传递性原则
原子性
一个操作要么全部执行成功,要么全部执行失败
1、单指令原子操作
2、利用锁的组合指令原子操作
有序性
指令重排在单线程环境下不会出现什么问题,但是在多线程环境下,可能导致有的代码执行顺序修改后可能会导致与顺序执行的结果不同
这里可以使用Happens-Before原则
来解决问题