JMM内存模型
1.计算机结构
输入设备:就是我们的鼠标,键盘
存储器:对应的就是我们的内存,缓存
运算器和控制器共同组成了cpu
而输出设备就比如显示屏,打印机。
我们重点来聊一下缓存:
2.缓存
其实,当我们说计算机运行效率低下,速度慢,往往不是cpu的锅。而问题所在一般都是内存访问速度太慢。
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。
于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存。
CPU缓存模型如图下图所示。
运行速度: L1cache >L2cache >L3cache >内存
所以,系统会先访问L1缓存>L2缓存>L3缓存>内存
具体的速度如下:
3.java内存模型概念
Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
具体如下:
Java内存模型如上面所示:规定了工作内存和主内存的概念及其交互。
对共享数据的可见性、有序性、和原子性的规则和保障。
工作内存和主内存可能在很多地方(cpu寄存器 缓存 或者主内存)
4.主内存和工作内存的交互
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
具体操作如下图:
5.对主内存操作的三大问题
原子性问题描述
解决原子性问题
上锁!
可见性问题描述
可见性问题指的是,因为JMM内存模型规范,线程访问主内存的数据后会将数据复制到一个自己的共享内存里,若此时将主内存里的数据更改,就会引起数据不一致的可见性问题。
可见性问题的解决方法
用volatile关键字
static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){ // ....
} });
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
指的注意的是synchronized也可以保证代码内变量的可见性
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){ // ....
System.out.printIn();
} });
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
代码也会停下来
但性能更低。。。
有序性问题描述
我们来看如下代码:
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
}else {
r.r1 = 1;
} }
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
它有集中结果呢?
- 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
- 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
- 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
这些都是基于我们上面学的分析出来的结果,但实际上结果还有一个 0
线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
出现这种结果,说明发生了指令重排,引起了有序性问题。
有序性问题解决办法
有序性的理解
有序性引起的double-check问题
理解下就是:
这是优点:double-checke机制可以减少重复创建对象的现象,提升效率。
但是忽略了有序性问题
我们创建对象时的字节码如下:
0: new #2 // class cn/itcast/jvm/t4/Singleton 分配空间
3: dup //将引用地址放进操作数栈
4: invokespecial #3 // Method "<init>":()V 将对象完善
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton; 将完善后的对象放进局部变量表
此时容易发生 4 ,7之间的指令重排,使得对象没完善之前就赋值给了INSTANCE对象,如果对象本来就很复杂,后面的线程就容易得到不完善的对象。
解决办法:
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
} } }
return INSTANCE;
} }
happens-before
CAS 与 原子类
1.CAS
乐观锁的效率高于没优化之前的synchronized的效率,原理是不断比较旧值,确保旧值没被更改后赋值。
-
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
-
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。