深入理解Java 内存模型

最近在看周志明老师的《深入理解java虚拟机》和《并发编程艺术》,有一些收获分享给大家。也看到一些相关的博客,写的都很好。强烈推荐java后端的同行看看这两本书。此篇想思维发散的说说java内存模型。也总结下自己就的收获。欢迎大家讨论,如若有不对的地方,还请指正,先谢过了。

1、物理计算机的内存模型

    java 内存模型出现的原因:虚拟机规范试图制定一种内存模型来屏蔽掉各种硬件和操作系统的差异,达到不同平台统一的内存访问效果。如果直接使用物理硬件和操作系统的内存模型,可能会导致相同程序在不同的平台有不同的表现。下面就从物理计算机的内存模型开始说说我们的java内存模型。

1.1 出现的原因

    自计算机出现以来,速度一直就是人们追求的目标。由于计算机的运算速度和它的存储和通信子系统速度差距太大。大量的时间都花费到了等待数据的过程。所以多任务处理成了现代计算机必备的功能了。有了多任务处理计算机,人们发现计算机还是不够快。原因就是即使有多任务处理,但是绝大多数的计算都不能只靠处理器完成,需要去内存读取运算数据,存储运算结果等。这些io操作很难消除。又因为存储设备与运算速度有很大的差距。所以计算机不得不加入了一层读写速度尽可能接近处理器运算速度的高速缓存。这样处理器就不用花费很长时间等待内存读写了。

    解决了这个问题,紧接着又出现了一个新的问题,就是缓存一致性的问题。大概就是这样一个情况,在多处理器的计算机中,每个处理器都有自己的高速缓存,而他们有共享同一主内存。当不同的处理器的运算任务都涉及到同一内存区域的时候。将可能导致缓存数据不一致。如果不一致了,那么同步会主内存的以谁的缓存数据为准呢。出了问题就要解决啊,缓存一致性协议就出现了。缓存一致性协议就不多说了。搜索下有很多资料。这类协议有很多,例如MESI、MOSI、Firefly等。内存模型可以定义为在特定协议下对内存或高速缓存进行读写访问的抽象。不同架构的物理机可以拥有不同的内存模型,java想达到平台无关性,就要定义自己的内存模型达到不同平台下相同的内存访问效果。

2、java 内存模型

    java内存模型存在很长时间了。经过长时间的验证,在jdk1.5 的时候java内存模型完善和成熟了。

    java内存模型规定了所有的变量都存储在主内存,每条线程有自己的工作内存,与高速缓存类似工作内存中存储该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作都需要在工作内存中完成,不能直接读取主内存中的变量。不同线程也不能访问对方的工作内存,交互只能通过主内存来完成。这样java内存模型的结构出来了,如下图(图懒得画了,在网上找了一个^_^)。


但是怎么定义主内存和工作内存的交互协议呢。(就像是物理计算机加入了高速缓存,如果没有缓存一致性协议,那不就乱了吗)所以java内存模型也定义了以下八种操作和规则来完成。另外虚拟机必须保证下面提及的操作都是原子的不可再分的。

- lock(锁定):作用于主内存的变量,锁定一个主内存中的变量,标志该变量为一条线程独占的状态。

- unlock(解锁):作用于主内存的变量,清除变量的独占状态,只有清除了独占状态,其他线程才能lock 该变量。

- read: 作用于主内存的变量,把主内存的一个变量的值从主内存传输到线程的工作内存,以便后来的load操作使用。

- load: 作用于工作内存的变量,把read回来的值放入工作内存的变量副本中。

- use: 把变量传给执行引擎使用。

- assign: 把从执行引擎接收到的值赋值给工作内存中的变量。

- store :作用于工作内存的变量,把一个变量的值传送到主内存,以便后面的write 使用。

- write : 作用于主内存的变量,把store的变量放入到主内存中的变量。

好了,上面的八种操作定义完了,定义完就可以了吗?我们说java内存模型要定义主内存和工作内存的交互协议(类比物理机的缓存一致性协议)来解决缓存一致性的问题,上面定义的八种操作,只是把操作定义出来了,但是如何做呢?java内存模型还定义了执行上述操作需要遵循下面的协议,如果上面的八种操作符合下面定义的规则就不会出现缓存不一致的问题:

- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存取了工作内存不接受。或者工作内存会写,主内存不接受的情况。

- 不允许一个线程丢弃它最近的assign操作,即工作内存修改了必须把该变化同步会主内存。

- 一个新的变量必须在主内存生成

- 一个变量在同一时刻,只能有一个线程对其进行lock操作,但同一个线程可以执行多次。

- 对一个变量进行lock操作,会清工作内存中此变量的值,下次再使用的时候需要进行重新的read和load。

- 线程只能unlock 自己的lock操作。且不能在没执行lock操作的时候执行unlock操作。

- 对一个变量执行unlock 操作前,必须要把变量同步回主内存。

- volatile 规则(另外的文章会说)

    这样操作和规则都定义完了,只要这些操作符合上述定义的规则,那么就不会出现并发不安全的情况。(也就是解决了虚拟机层面的缓存一致性问题)。可能会有点繁琐,下面有一个简单的原则来定义什么情况下不会出现缓存不一致的问题。

3、先行发生原则

    先行发生原则总觉得有点绕口,先行发生原则是上述所说的操作和规则的另一种说法或者是简单理解的版本。只有这些先行发生原则或是可以通过这些原则推断出来的是线程安全的。

    先行发生:先行发生说的是一种现象,现象描述的是A操作在时间上先发生于B操作,那么A操作所做的改变B都能看到。例如A操作把一个变量赋值为1,A执行完后B执行的时候也能看到这个变量的值为1。

    只要符合了下面说的原则的任意一条,就不用担心会出现缓存一致性的问题(也就是线程是安全的)。

    - 程序次序规则:一个线程内,书写在前面的操作先行发生于书写在后面的操作。

    - 管程锁定规则: 一个unlock操作先行发生于后面对于同一个锁的lock操作。

    - volatile变量规则:对于一个volatile的写操作先行发生于后面对于这个变量的读操作。

    - 线程启动规则: start()先行发生于这个线程的每个动作

    - 线程终止规则:  线程中的所有操作先行发生于对此线程的终止检测。(终止检测:Thread.join(),Thread.isAlive())

    - 线程中断规则:对线程的interrupt()方法先行发生被中断线程的代码检测到中断事件的发生。

    - 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法。

    - 传递行: 先行发生可以传递。A>B B>C  => A>C

4、总结

    由于物理计算机追求速度,导致高速缓存的出现,高速缓存的出现带来了缓存一致性的问题。不同架构的物理计算机使用了不同的内存模型解决了缓存一致性。我们java程序要运行在物理计算机,又要具有平台无关性。那么只能自己定义一个内存模型来屏蔽掉物理机不同的内存模型带来的影响。并且要解决缓存一致性的问题。

综上所属,java内存模型解决了两个问题。

1,平台无关行

2,缓存一致性

猜你喜欢

转载自blog.csdn.net/wlittlefive/article/details/79434657