不再迷惑的Java内存模型

一直以来,java内存模型都是让人很迷惑的东西,很多书籍,博客都有对它的相关介绍,很多人读完后,还是搞不清楚。而且这个东西和JVM的内存结构又很容易混淆,很多博客讲解内存模型都会当成JVM的内存结构去讲,甚至我身边有着好几年java工作经验的人,都搞不明白,说一些堆,虚拟机栈啥的。所以呢,今天就和读者一起来探究java内存模型到底是什么东西。这个模型是干嘛用的。

一说到模型,我相信很多人脑海中就会想象出一个具体的模型画面出来,由什么样的结构组成啊。但是,java内存模型却并不是这样的,所以才让很多的java学习者感到难以理解。在介绍Java内存模型之前,先说一下内存模型这个东西,在《深入理解Java虚拟机》中介绍 这是在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。怎么样,这个解释是不是很让人懵逼。

内存模型,实际上这是一个跟计算机硬件有关的概念。计算机在执行程序时,每条指令都是由CPU去执行的。在执行的时候,数据是存放在主存中,也就是计算机的物理内存,每次要将数据从主存读取到CPU中,再去执行,把执行后的结果再写回到内存中。但是随着CPU的执行速度越来越快,从内存中读取和写入数据的速度和CPU的执行速度相比,差距就越来越大,这样就导致了CPU效率低下,每次不得不耗费很多时间来等待数据的读取和写入。

后来,人们在CPU和内存之间加入了高速缓存,现在的CPU中已经包含了高速缓存。缓存就是保存一份数据的拷贝,特点就是速度快,内存小,而且很贵。这样以后程序执行就变成了这个样子:程序运行时,先将需要的数据从主存中拷贝一份到高速缓存,CPU进行计算可以直接从它的高速缓存中读取和写入数据,再将高速缓存中的数据刷新到主内存中。在后来计算机从单核CPU发展到多核CPU,也开始支持多线程。这样就会出现缓存不一致的问题。我们来看一下,这个问题是什么,当多个线程在不同的核心上分别执行,每个核心都会在各自的缓存中保留一份共享内存的副本,这样由于多核时可以并行的,可能会出现多个线程同时对各自的缓存进行写操作,这样各自的缓存之间的数据就会出现不一致的情况。举个例子:一家由多个合伙人创办的公司,合伙人A觉得员工小王天天偷懒,溜须拍马,打算辞退他,就让自己的秘书去办这件事,合伙人B觉得员工小王说话做事有一套,打算给他升职,也让自己的秘书去办这件事,结果,A的秘书办事比较快,立马就通知人事把小王开了,A就已经知道了小王被开除,此时,B还不知道小王被开,B的秘书也去办,这样就出了问题。

这种基于高速缓存的存储交互很好地解决了CPU与内存的速度矛盾,但是它引来了一个新的问题:缓存一致性。为了解决这个问题,需要各个处理器访问缓存时都要遵循一些协议,在读写时根据协议来进行操作,这类协议有MSI,MESI,Synapse等。除了这种情况,还有一种硬件问题也比较重要,那就是为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的。这就是指令重排序优化。

上面所提到的缓存一致性,和指令重排序问题都是随着硬件的不断升级导致的,有什么办法能够解决这些问题呢。在这里就引入了内存模型这个概念,就是为了保证共享内存的正确性,定义了共享内存中多线程读写操作行为的规范。那么什么是Java内存模型呢,在《深入理解Java虚拟机》中是这样介绍的,Java内存模型是可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致内存访问效果这样一种规范。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存去完成。它们之间的交互关系如图所示:

在主内存与工作内存交互的细节上,java内存模型定义了以下8种操作来完成。这8种操作都是原子的。分别是lock(锁定),unlock(解锁),read(读取),load(载入),use(使用),assign(赋值),store(存储),write(写入)。如果要把一个变量从主内存复制到工作内存,那就要顺序执行read和load操作,如果要把变量从工作内存同步到主内存,就要顺寻执行store和write操作,同时java内存模型还规定了在执行这8种基本操作需要满足的规则,这里我就不再详述了,可以去看一下《深入理解Java虚拟机》。

另外,Java提供了一系列和并发处理相关的关键字,比如volatile,synchronized,final,concurrent包,这些关键字实际上就是java内存模型封装了底层的实现提供给开发者使用,从而不需要关心底层编译器优化,缓存一致性等问题,保证并发场景中的原子性,可见性,有序性。这些关键字的介绍,网上都有很多相关资料,读者可以自行去查看,

下面来介绍一下原子性,可见性,有序性这三个特性。

原子性:这个大家应该都很好理解,就是保证一个或一组操作是不可中断的,要么完成,要么不发生。在Java中可以使用synchronized关键字来保证方法或代码块的操作是原子性的。

可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,volatile,synchronized,final都可以实现可见性。

有序性:就是程序执行的顺序按照代码的先后顺序去执行。java内存模型允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。java提供了volatile,synchronized这两个关键字来保证线程之间操作的有序性。

如果Java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么是不是很繁琐呀,实际上,Java语言里有一个先行发生(happens-before)的原则。这个原则是判断数据是否存在竞争,线程是否安全的重要依据。先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,那么操作A产生的影响能被操作B观察到。这个影响包括了修改了内存中共享变量的值,发送了消息,调用了方法等。

总结一下:Java内存模型是一种规范,目的是解决多线程通信之间造成的缓存一致性,编译器和处理器的指令重排序所带来的问题,从而保证并发场景下的原子性,可见性,有序性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值