Java 内存模型
Posted 皓洲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 内存模型相关的知识,希望对你有一定的参考价值。
Java 内存模型
参考视频:https://www.bilibili.com/video/BV1F64y1B7sV
参考博客:https://zhuanlan.zhihu.com/p/29881777
硬件内存模型
周所周知:CPU的处理速度和内存的读写读写速度是不在一个数量级的,所以需要CPU在内存之间加上缓存来进行提速,这就呈现了一种CPU-寄存器-缓存-主存的访问结构。
多CUP缓存产生的同步问题
当一台计算机出现了多个CPU的时候就会出现如下的问题:
- 假如CPU A将数据D从主存读取到独占的缓存内,将D改为D1
- 同时CPU B将数据D从主存读取到独占的缓存内,将D改为D2
- 然后A B同时将数据D写入主存中,那么主存的值会变成D1还是D2?
针对这个多个CPU缓存之间的同步问题,科学家们制定了缓存一致性协议。
CPU指令重排
关于缓存一致性协议,可以猜想的是,其内容一定是一些和数据同步相关的操作。
既然要进行数据同步,就很可能出现等待、唤醒这样的操作,这可能导致性能问题,尤其是对于CPU这种运算速度极快的组件来说,丝毫的等待都是极大的浪费。
比如CPU B想要读取数据D的时候,还要等待CPU A将D写回主存。这种行为是难以忍受的。
因此,计算机科学家们做了一些优化,怎么优化呢?整体思想就是将同步改成异步。
比如CPU B要读取数据D时,发现D正在被其他CPU进行修改,那么此时CPU B注册一个读取D的消息,自己回头去做其他的事情,其他CPU写回数据D后 响应了这个注册消息。此时CPU B发现了这个消息被相应后,再去读取D 这样就能够提升效率。
但是对于CPU B来说,程序看上去就不是顺序执行的了,有可能会出现先运行后面的指令,再回头去运行前面的指令,这一种行为就体出了一种指令重排序
Java内存模型
Java内存模型屏蔽了各种硬件和操作系统的内存访问差异,实现了让Java程序能够在各种硬件平台下都能够按照预期的方式来运行。
它的抽象如图所示:
这里说明一下:
Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。
我们可以将工作线程和本地内存具象化为Thread Stack,将主存具象为Heap
Thread Stack有两种类型的变量:
- 原始类型的变量,比如(int、char等)总是存储在线程栈上。
- 对象类型变量,引用(指针)本身存储在线程栈上,而引用指向的对象存储在堆上。
在Heap中存储对象本身,持有对象引用的线程就能够访问该对象,Heap本身不关心哪个线程正在访问对象。
我们可以这么理解:Java线程模型中Thread Stack和Heap都是对物理内存的抽象。
Thread Stack大部分都是使用寄存器和CPU缓存来实现的,而Heap中需要储存大量的对象,需要大量的容量,所以大部分是使用主存来实现的。
线程通信中可能存在的问题
-
可见性问题
假如本地内存A和本地内存B中存在着数据x的副本,且x值为1,当线程A将X修改为2并且将数据写入主存后,当线程B想要读取X,默认的会从本地内存B中读取,而本地内存B的x依然是等于1的。换言之,线程A刷新了主存中x的值,线程B却不知道x的值发生了改变。
-
原子性问题
假如线程A和线程B同时读取主存中x的值,此时x为1。当线程A和线程B对x进行自增1后,放回主存,此时主存中x值变成2,但是x经过线程A和线程B的两次自增,x的值应该变为3。
并发三要素
这些问题呢,被总结为三个要素:可见性、原子性、有序性
可见性
可见性指的是,当一个线程修改共享变量的值,其他线程需要能够立即得知这个修改。
-
解读一
线程A修改了数据D,线程B需要督导修改后最新的D。(由刷新主存的时机引起的)
写一个例子:线程T1,在a!=2的时候进行死循环,一秒钟后,线程T2将a的值改为2。此时如果线程T1能够读到a的值被改为2,那么会跳出死循环。可是事实上线程T1会一直进行死循环。可以证明他们不满足可见性
static int a = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (a!=2);//只要a!=2就死循环
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
a=2;
}
});
t1.start();
Thread.sleep(1000);
t2.start();
}
解决方法1:使用volatile关键字。写volatile变量,主动写主存;读volatile变量,主动读主存。
解决方法1:使用synchronized关键字。synchronized块内部读写变量,隐式调用内存lock,unlock指令,主动读主存。
-
解读二
线程B需要读到被修改的变量D,线程A应该修改但是因为重排序导致线程A没有及时修改变量D。(由指令重排序引起的)
举个例子:下面代码执行到代码4时,变量b是否等于1?
static int a = 0;
static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a=1; // 代码1
flag=true; //代码2
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
if(flag){ // 代码3
int b = a; // 代码4
}
}
});
t1.start();
t2.start();
}
答案是否定的,我们在最上面提到过硬件内存模型中存在者指令重排序机制,Java内存模型中也存在指令重排序,它们的作用和约束都是一样的,第一是为了更高的执行效率,第二是在单线程中重拍后能够保证程序执行结果的正确性,就是说和顺序执行的结果是一样的。
解决方法1:使用volatile关键字。禁止volatile变量与之前的语句进行重拍,volatile这行相当于“基准线”当运行到基准线,能保证之前语句和自身的可见性。将flag加上volatile关键字就可以解决问题了。
解决方法1:使用synchronized关键字。synchronized块内部读写变量,隐式调用内存lock,unlock指令,主动读主存。使用synchronized将代码1和代码2进行包裹,这样其内部无论如何进行重排,外部只能看到最终结果,这样也保证了可见性。
原子性
原子性指的是,一个操作时不可中断的,要么全部执行,要么全部失败。
- 单指令原子操作
- 利用锁机制的原子操作,反应到上层就是synchronized关键字
有序性
上文其实我们已经详细讲解到了无论是从硬件内存模型还是Java内存模型来看,都支持指令重排这种优化操作,在单线程中虽然指令可能会被重排序,但是在单线程中内存模型能够保证执行结果的准确性,也就是说在单线程中无论指令如何重排,他最终的执行结果和顺序执行的结果时一样的,但是在多线程环境下就可能因为重排而导致一些问题。
我们在上面就提到过了由于指令重排引起的可见性问题,所以在这里不在继续讲了。
以上是关于Java 内存模型的主要内容,如果未能解决你的问题,请参考以下文章