前言
网上有很多关于JAVA内存模型的文章,但大多只讲解了JAVA内存模型本身的概念,很少提到JAVA内存模型出现的原因是什么,导致看完之后自己的思维更加混乱。本文简单介绍为什么要有内存模型以及JAVA内存模型的基本概念供参考学习。
一、为什么要有内存模型
1.1 CPU和缓存一致性
我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。
刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。
例如:你是cpu,小伙伴是内存,刚开始你和你的小伙伴其乐融融,后来你读到了博士,你的每一个命令,由于小伙伴的理解能力和执行能力欠缺,就会耗费很多时间,这就无形拖慢了效率。
可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧。
解决办法:
在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。
那么,程序的执行过程就变成了:
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
但是这会出现什么问题?
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
1.2线程切换和指令重排
Java 并发程序都是基于多线程的,自然也会涉及到任务切换,任务切换的时机大多数是在时间片结束的时候。这种切换可能会导致问题。
两个线程 A 和 B 同时执行 count++, 即便 count 使用 volatile 修辞,我们预期的结果值是 2,但实际可能是 1。
编译器为了优化性能,有时候会改变程序中语句的先后顺序,比如Java虚拟机的即时编译器(JIT)也会做指令重排。
如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。
1.3并发编程的核心问题
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性即程序执行的顺序按照代码的先后顺序执行。
缓存一致性问题其实就是可见性问题。而线程切换是可以导致原子性问题的。指令重排即会导致有序性问题。
二、JAVA内存模型(JMM)
前面提到的,缓存一致性问题、指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?
为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
三、JAVA内存模型的实现
Java内存模型封装了底层的实现后提供给程序员使用的一些关键字:比如volatile、synchronized、final、concurrent包等。
原子性:在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized。
可见性:Java中的volatile关键字提供了一个功能,那就是被其修饰的变量修改后将新值同步回主内存,变量读取前从主内存刷新变量值。因此,可以使用volatile来保证多线程操作时变量的可见性。
有序性:在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。volatile关键字会禁止指令重排,synchronized关键字保证同一时刻只允许一条线程操作。
总结
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序等带来的问题。除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。