Java内存模型详解

在前面一篇文章Java运行时数据区域中提到Java内存模型,是用来控制线程之间的通信。很好奇Java内存模型是如何控制线程之间通信的,带着疑问开始今天的学习。

为什么需要线程同步

在解决这个问题之前,首先得明白,多线程是如何实现的?

通过CPU的时间分片机制,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,给人的感觉,就好像多个任务在同时运行。线程之间会进行资源的抢占,例如,当线程A在对某个变量n进行操作还没有完成,另外一个线程B就进来了,那么此时B线程获取到的变量n就是线程A还没有操作完的变量,这样就会造成数据的脏读,产生线程安全问题,所以需要线程同步,确保多线程之间的变量的可见性。因此线程同步是保证线程安全的前提。

关于重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三类:

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

由于在编译和执行期重排序的存在,使得各个线程之间并不是按照java语句得顺序先后执行,而是随机执行得,这也是导致了非同步多线程得安全问题得原因之一。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

 处理器重排序

处理器并不直接向内存中写入数据,而是通过向高速缓冲区写入数据,然后由高速缓冲区向内存写入数据,这样可以保证运行效率,而不必一直等待写操作完成才去执行相应得操作,所以处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

Java内存模型

Java内存模型是就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题[1]

原子性[2]

线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。即存在原子性问题。

缓存一致性[2]

在多核CPU,多线程的场景中,每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

有序性[2]

除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。

针对上面的这些问题,不同的操作系统都有不同的解决方案,而Java语言为了屏蔽掉底层的差异,定义了一套属于Java语言的内存模型规范,即Java内存模型

Java内存模型的实现

  • 共享内存:使用volatile关键字

使用volatile关键字来修饰变量,volatile关键字会禁止指令重排,改变了变量的可见性。一个线程修改了变量,另一个变量会感知并执行相应的业务。实际上,使用volatile修饰的变量,线程每次在使用变量时,不从寄存器取值,而是从主存当中取得变量的值,相应的,当一个线程对变量进行了修改,也会将修改之后的变量写入主存,正式因为如此,才会保证线程取到变量的值是最新的。

 

图1 共享内存线程间通信示意图

  • 消息传递:使用Object类的wait()方法和notify()方法

wait和notify的使用必须在同一synchrnoized范围内。synchronized关键字保证同一时刻只允许一条线程操作。但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

图2 使用Object类的wait()方法和notify()方法线程间通信示意图

总结

Java内存模型,其实是保证了Java程序在各种平台下对内存的访问都能够得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。

 参考:

[1] 周志远, 张大方, 缪力,等. 基于Java内存模型的并发程序模型检测[C] 中国测试学术会议. 2008:111-114.

[2] 深入理解Java虚拟机[M]. 2011.

posted @ 2019-08-20 09:23  下雪不下雨  阅读(142)  评论(0编辑  收藏  举报