大家好,我是鲲哥!上周在极客时间的一门课程中看到一位讲师在讨论ConcurrentHashMap(以下简称CHM)是强一致性还是弱一致性时提到了这个问题。这个解释在网上也广为流传,所以不管真假,在回答这个问题之前,我们得想清楚两个问题。什么是强一致性,什么是弱一致性。上面说了get没有加锁,所以不能立即获取到put的数据,也就是说加了锁就可以立即获取到put的值了?那么除了加锁之外,还有其他方法可以立即获取它的投入值吗?强一致性和弱一致性强一致性首先我们来看第一个问题,什么是强一致性。Consistency是指Replications问题中的数据一致性。可分为强一致性和弱一致性。强一致性也可以称为原子一致性(AtomicConsistency)或线性一致性(LinearizableConsistency),必须满足以下两个要求。任意一次读取,都可以立即读取某条数据的最新写入数据。系统中所有进程看到的操作顺序与全局时钟下的顺序一致。简单的说,假设有两个线程A和B对同一个数据集进行操作,并且假设A先修改了操作,那么在时间上发生在A的操作之后的所有B的操作应该能够立即(或实时)看到A的修改操作的结果。弱一致性和强一致性的对立面是弱一致性,即数据更新后,如果立即访问,可能无法访问或者只能访问部分数据。如果线程A在更新数据后一段时间后还能访问到数据,这种情况称为最终一致性,最终一致性也是弱一致性,只是弱一致性的一种特例。那么Java中出现弱一致性的原因是什么,或者保证强一致性的方法有哪些呢?这需要理解两个概念,可见性和顺序。一致性的根本原因:可见性和顺序可见性首先我们需要了解Java中的内存模型。上图展示了JVM中的Java内存模型。可以看出主要由两部分组成,一个是线程特有的程序计数器,虚拟机栈,本地方法栈。由于这部分数据是线程独有的,所以不存在一致性问题(我们常说的一致性问题是指多个线程之间的数据一致性),其中一部分是线程共享的堆和方法区。让我们关注堆内存。我们知道线程执行占用CPU,CPU从寄存器中取数据。如果寄存器中没有数据,则必须从内存中取出。众所周知,两者之间的速度差异是巨大的,所以为了缓解这个矛盾,CPU内置了三级缓存。每次线程执行需要数据时,堆内存中的数据都会以cacheline(通常为64Byte)的形式加载到CPU的L3缓存中,这样后面取的数据就可以直接从缓存中取,大大提高了CPU的执行效率(如下图)。但是在这种情况下,线程加载并执行完数据后,数据往往会缓存在CPU寄存器中,并不会立即刷新到内存中,这样其他线程如果需要共享数据,就得不到最新的数据堆内存,导致数据不一致。例如,以下面代码的执行为例。//线程1执行的代码inti=0;i=10;//线程2执行的代码j=i;线程1执行完后i的值为10,然后2开始执行,此时j的值很可能还是0,因为线程1执行的时候会先加载i的值=0从内存到CPU缓存,再把10赋值给i。此时CPU缓存中更新了10,但没有刷新到内存中,当线程2开始执行时,它会先从内存中加载i的值(它的值为0)到CPU,所以它的值还是0,而不是10,这是典型的CPU缓存数据不一致的结果。那么如何解决可见性导致的数据不一致呢?其实只要让CPU在修改共享变量时立即回写到内存即可。这个值是可以读入的。有序性除了可见性导致的数据不一致,指令重排序也会导致数据不一致。公共类重新排序{私有静态布尔标志;私有静态整数;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){while(!flag){Thread.yield();}System.out.println(num);}},"t1");t1.开始();数=5;①标志=真;②}}很多人可能认为上面的代码执行步骤是按照正常的①、②、③执行的,但实际上很有可能是编译器改变了位置。实际的执行顺序可能是①③②,也可能是②①③,也就是说①③是紧挨着的。为什么会这样?因为执行完1之后,CPU会从内存中加载x=1到寄存器中。如果此时直接调用③执行,那么CPU可以直接读取寄存器中x的值1进行计算。反之,如果语句先执行②,则有可能寄存器中x的值被覆盖,导致执行③后x的值从内存中重新加载。有人可能会说这样的指令重排序似乎不是什么大问题,那么考虑下面的代码:publicclassReordering{privatestaticbooleanflag;私有静态整数;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){while(!flag){Thread.yield();}System.out.println(num);}},"t1");t1.开始();数=5;①标志=真;②}}上面代码最终输出的值正常是5,但是如果把上面①和②两行指令重新排序,结果可能是0,导致我们观察到的数据不一致的现象,所以显而易见的解决方法是为了避免指令重排序的发生,也就是保证指令按照我们看到的代码的先后顺序执行,也就是我们常说的有序性,一般通过在指令之间加入内存屏障来避免指令重排序那么如何确保能见度和秩序?相信大家都很熟悉了。使用volatile可以保证可见性和有序性。只要在声明属性变量的时候加上volatile,就可以让这个变量达到强一致性。也就是说,上面提到的Reordering类的flag只需要声明为volatile,那么打印出来的结果永远是5!那么问题来了,CHM是强一致性吗?首先我们以Java8为例,看一下它的设计结构(和之前的版本差别不大,主要是增加了一颗红黑树,提高了查询效率)。我们来看看表数组和节点是如何声明的(后面的定义8与之前版本相同):publicclassConcurrentHashMap
