深入理解 Java 内存模型(七)——一致性(Consistency)

可以说并发系统要解决的最核心问题之一就是一致性的问题,关于一致性的研究已经有几十年了,有大量的理论,算法支持。这篇说说一致性这个主题一些经常提到的概念,理清Java内存模型在其中的位置。

一致性问题更准确的说是一致性需求,看系统需要什么样的一致性保证。比如分布式领域的CAP理论说Consistency, Availability, Partition tolerance这三个要求同时只能满足两个,另外一个就要有所取舍。所以很多种场景下,Consistency可能只需要满足最终一致性,不用满足强一致性。

一致性问题在单机器单CPU的情况下是最简单的,由于只有一个CPU,所有的读写操作都可以按照全局的时间顺序执行

在单机器多CPU的情况下,多CPU并发执行,共用一个内存,一般通过共享内存的方式来处理一致性问题,通过定义满足不同一致性需求的内存模型来解决内存一致性问题(Memory Consistency)

在分布式环境中,多台机器多CPU通过网络来并发执行,一般通过消息通信的方式来处理一致性问题,比如分布式事务的多阶段提交,处理分布式存储的Paxos协议,ZooKeeper的Zab协议,处理的都是分布式存储场景下的数据一致性的问题。分布式环境中也有使用分布式共享内存的方式。

所以目前处理一致性问题主要有共享内存和消息通信这两个大的方式,每种方式里面又根据不同的需求有不同的实现方式。Java内存模型处理的就是单机器多CPU场景下的内存一致性问题

先来看看一致性的定义,这是冯诺依曼体系结构中对一致性的定义

Consistency: a read returns the most recently written value

一个读操作应该返回"最近"的一个写操作写入的值
但是"最近"(most recently)这个概念比较模糊,需要对其概念严格化,根据不同的严格化定义,这几十年来产生了多种不同的一致性定义,每种一致性定义要解决的场景也都有区别。 

1. 严格一致性 Strict Consistency 线性一致性 Linearizability

这是最严格的概念模型,定义了在应用场景中,所有的读写操作都按照全局的时序来排列执行,比如在单机器多核CPU的场景下,所有的CPU需要共享一个全局的时钟顺序,并且所有CPU的任意读写操作都要按照这个全局的时钟顺序执行,一旦新写入了一个值,那么这个值必须马上被其他所有的CPU都能看到。在分布式场景下,所有的分布式节点都要共享一个全局的时钟顺序来执行。

严格一致性要求写操作能够马上(instantaneously)被传播出去,任意执行的节点要马上可以看到这个新写入的值。这个模型在数学上是可行的,但是在物理上是难以实现的,而且即使实现也是最低效率的,所以大家看看就好

2. 顺序一致性 Sequential Consistency

顺序一致性不要求全局的时钟顺序,它只需要各个CPU局部的时钟顺序,它由三个要点

对每个单个CPU来说,它看到自己程序的执行顺序始终是和程序定义是一致的(单个CPU角度)
每个CPU看到的其他CPU的写操作都是按照相同的顺序执行的,大家看到的最终执行的视图是一致的(从全局的角度)
单个CPU对共享变量的写操作马上对其他CPU可见

 如下图的例子很能说明顺序一致性的特点

           

根据顺序一致性的特点,我们知道r1和r2的只能有这3种结果,因为顺序一致性允许不同的CPU并发执行,但是对单个CPU的指令来说是按照执行的程序顺序执行的,所以不会出现r1 = y先于x=1执行的情况。并且所有的处理器都只会看到同一个全局的执行顺序,要么Execution1,或者2,或者3,不会出现两个处理器看到不同的全局执行顺序的情况。这也就要求了单个处理器的写操作要马上被其他处理器可见。

从上面的例子我们可以看到严格的顺序一致性模型其实也是个概念模型,限制了编译器的优化空间。实际的实现中编译器做了大量的优化工作,这些优化工作的基础就是指令重排序操作,而指令重排序打破了这种严格的顺序一致性,比如单个处理器看到的指令执行顺序可以和它的程序定义顺序一致。

3. 因果一致性 Causal Consistency

因果一致性是一种弱的顺序一致性,只有有因果关系的数据才需要保证顺序一致性,没有因果关系的数据不需要保证顺序一致性,也就是说对于没有因果关系的数据不需要其他处理器看到一致的视图。

那么什么是因果关系呢?必须处理器A写了一个x = a, W(x)a,处理器B先读取x的值,再写x的值,R(x)a, W(x)b,那么对处理器B来说,它的写x操作和处理器A的写x操作 就有因果关系,因为它后写入的值可能依赖于处理器A先写入的值。这时候这两个操作要保持顺序一致性,也就是说其他处理器看到的顺序都是W(x)a, W(x)b。

实习因果一致性的实现复杂,需要额外的建立一个依赖关系图,即一个操作依赖于其他什么操作。

4. 处理器一致性/ PRAM(Piplined RAM) 管道式存储器

这两种一致性经常被放在一起,概念基本一致,他们比因果一致性更弱,只要求从一个处理器来的写操作按照同样的顺序被其他处理器看到,不同处理器的写操作可以按照不同的顺序被看到,也就是说它不保证有因果关系的写操作按照执行的顺序执行,拿上面因果一致性的例子来说,虽然W(x)a和W(x)b存在因果关系,但是对不同的处理器来说,它们可以先看到a也可以先看到b。

它的优点是同一个处理器的写操作被管道化,相当于用管道串行了,并且隐藏了写操作的延迟。比如一个处理器的写操作可能还在写缓存区没有刷新到内存被其他处理器看到,这个处理器的读操作可以马上进行对这个变量的读操作,而不需要等待它的写操作完全被写入内存,大大提高了系统的性能。不同处理器的写操作是并行执行的

处理器一致性还是比较严格的一致性模型,因为同一个处理器的写操作还是严格按照顺序执行。而重排序的优化可以对没有数据相关性的写操作进行重排序。

上面这几种一致性模型处理的问题域是对所有的共享变量而言,下面三种一致性模型是针对有明确定义的同步变量而言,可以理解为Java中的volatile变量,内置锁的获取/释放

5. 弱一致性 Weak Consistency

弱一致性只对被同步操作保护的共享变量而言,规定了只有对共享变量的同步操作完成之后,共享数据才可能保持一致性.在同步操作过程中,是不保证一致性的,单个处理器对共享变量的修改对其他处理器是不可见的。相比与严格的顺序一致性,它只保持了执行顺序上的顺序一致性,至于可见性必须要等待同步操作结束

对同步变量的读写按照顺序一致性
只有所有对同步变量的写操作完成之后才能对同步变量进行访问
只有所有对同步变量的访问(读/写)完成后才能对同步变量访问


6. 释放一致性 Release Consistency

弱一致性的粒度太大,包含了进入同步操作和释放同步操作两部分,而只有同步操作整体完成后,其他处理器才有可能保持一致性。 释放一致性规定了对同步变量的释放操作后,就对同步变量的状态广播到其他处理器

7. 进入一致性 Entry Consistency

和释放一致性一样,也是为了减小弱一致性的粒度,进入同步变量时,获取同步变量的最新状态

所以如果一个共享变量要被同步操作保护,那么所有操作它的地方都要被同步保护,否则就不保证一致性

8. 缓存一致性 Cache Consistency

缓存一致性的语义和上面的数据一致性模型有些区别,它主要说的是多个CPU缓存之间的一致性协议,我们要知道的是现代CPU基本都提供了缓存一致性的实现,比如一个CPU修改了一个缓存,那么其他CPU可以马上看到修改的缓存数据。详细内容请参考缓存一致性问题和缓存一致性协议MESI

发布了169 篇原创文章 · 获赞 6 · 访问量 3513

猜你喜欢

转载自blog.csdn.net/weixin_42073629/article/details/104742567