Java并发之内存模型(JMM)浅析

2019-08-20  本文已影响0人  Astrophel_06c5

背景

学习Java并发编程,JMM是绕不过的槛。在Java规范里面指出了JMM是一个比较开拓性的尝试,是一种试图定义一个一致的、跨平台的内存模型。JMM的最初目的,就是为了能够支多线程程序设计的,每个线程可以是和其他线程在不同的CPU核心上运行,或者对于多处理器的机器而言,该模型需要实现的就是使得每一个线程就像运行在不同的机器、不同的CPU或者本身就不同的线程上一样,这种情况实际上在项目开发中是常见的。简单来说,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。(当然你要是想做高性能运算,这个还是要和硬件直接打交道的,博主之前搞高性能计算,用的一般都是C/C++,更老的语言还有Fortran,不过现在并行计算也是有很多计算框架和协议的,如MPI协议、基于CPU计算的OpenMp,GPU计算的Cuda、OpenAcc等)当然了,JMM在设计之初也是有不少缺陷的,不过后续也逐渐完善起来,还有一个算不上缺陷的缺陷,就是有点难懂。

什么是JMM

  • JMM即为JAVA 内存模型(java memory model)。Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节的实现规则。它其实就是JVM内部的内存数据的访问规则,线程进行共享数据读写的一种规则,在JVM内部,多线程就是根据这个规则读写数据的注意,此处的变量与Java编程里面的变量有所不同步,它只是包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量方法参数(局部变量和方法参数线程私有的,不会共享,当然不存在数据竞争问题)
  • 如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

JMM和JVM有什么区别

JMM核心知识点

JMM内存模型
这上如可以看见java线程中工作内存是通过cache来和主内存交互的,这是因为计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。

线程和线程之间想进行数据的交换一般大致要经历两大步骤:1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去;2.线程2到主内存中去读取线程1刷新过的共享变量,然后copy一份到工作内存2中去。(当然具体实现没有这么简单,具体的操作步骤在下文细讲)

三大特征

1. 原子性

2.可见性

3.有序性

八种基本内存交互操作

JMM定义了8种操作来完成主内存与工作内存的交互细节,虚拟机必须保证这8种操作的每一个操作都是原子的,不可再分的。(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

现在我们模拟一下两个线程修改数据的操作流程。线程1 读取主内存中的值oldNum为1,线程2 读取主内存中的值oldNum,然后修改值为2,流程如下

image

    从上图可以看出,实际使用中在一种有可能,其他线程修改完值,线程的Cache还没有同步到主存中,每个线程中的Cahe中的值副本不一样,可能会造成"脏读"。缓存一致性协议,就是为了解决这样的问题还现,(在这之前还有总线锁机制,但是由于锁机制比较消耗性能,最终还是被逐渐取代了)。它规定每个线程中的Cache使用的共享变量副本是一样的,采用的是总线嗅探技术,流程大致如下
    当CPU写数据时,如果发现操作的变量式共享变量,它将通知其他CPU该变量的缓存行为无效,所以当其他CPU需要读取这个变量的时候,发现自己的缓存行为无效,那么就会从主存中重新获取。
     volatile 会在store时加上一个lock写完主内存后unlock,这样保证变量在回写主内存时保证变量不被别的变量修改,而且锁的粒度比较小,性能较好。

Volatile

作用

    保证了多线程操作下变量的可见性,即某个一个线程修改了被volatile修饰的变量的值,这个被修改变量的新值对其他线程来说是立即可见的。
    线程池中的许多参数都是采用volatile来修饰的 如线程工厂threadFactory,拒绝策略handler,等到任务的超时时间keepAliveTime,keepAliveTime的开关allowCoreThreadTimeOut,核心池大小corePoolSize,最大线程数maximumPoolSize等。因为在线程池中有若干个线程,这些变量必需保持对所有线程的可见性,不然会引起线程池运行错误。

缺点

对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作(自增操作是三个原子操作组合而成的复合操作)不具有原子性,原因就是由于volatile会在store操作时加上lock,其余线程在执行store时,由于获取不到锁而阻塞,会导致当线程对值的修改失效。

原理

底层实现主要是通过汇编的lock的前缀指令,他会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存,lock前缀指令实际上相当于一个内存屏障(也可以称为内存栅栏),内存屏障会提供3个功能

    1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    1. 它会强制将对缓存的修改操作立即写入主存;
    1. 如果是写操作,它会导致其他CPU中对应的缓存行无效(MESI缓存一直性协议)。

总结

    JMM模型则是对于JVM对于内存访问的一种规范,多线程工作内存与主内存之间的交互原则进行了指示,他是独立于具体物理机器的一种内存存取模型。
    对于多线程的数据安全问题,三个方面,原子性、可见性、有序性是三个相互协作的方面,不是说保障了任何一个就万事大吉了,另外也并不一定是所有的场景都需要全部都保障才能够线程安全。

参考资料

https://www.cnblogs.com/lewis0077/p/5143268.html
《java并发编程》

上一篇 下一篇

猜你喜欢

热点阅读