加餐讲什么是数据的强、弱一致性 - 图1加餐讲什么是数据的强、弱⼀致性

你好,我是刘超。

加餐讲什么是数据的强、弱一致性 - 图2第17讲讲解并发容器的时候,我提到了“强⼀致性”和“弱⼀致性”。很多同学留⾔表示对这个概念没有了解或者⽐较模糊,今天这讲加餐就来详解⼀下。

说到⼀致性,其实在系统的很多地⽅都存在数据⼀致性的相关问题。除了在并发编程中保证共享变量数据的⼀致性之外,还有数据库的ACID中的C(Consistency ⼀致性)、分布式系统的CAP理论中的C(Consistency ⼀致性)。下⾯我们主要讨论的就是“并发编程中共享变量的⼀致性”。

在并发编程中,Java是通过共享内存来实现共享变量操作的,所以在多线程编程中就会涉及到数据⼀致性的问题。

我先通过⼀个经典的案例来说明下多线程操作共享变量可能出现的问题,假设我们有两个线程(线程1和线程2)分别执⾏下
⾯的⽅法,x是共享变量:

//代码1
public class Example { int x = 0;
public void count() {
x++; //1
System.out.println(x)//2
}
}

加餐讲什么是数据的强、弱一致性 - 图3

如果两个线程同时运⾏,两个线程的变量的值可能会出现以下三种结果:
加餐讲什么是数据的强、弱一致性 - 图4

Java存储模型

2,1和1,2的结果我们很好理解,那为什么会出现以上1,1的结果呢?

我们知道,Java采⽤共享内存模型来实现多线程之间的信息交换和数据同步。在解释为什么会出现这样的结果之前,我们先通过下图来简单了解下Java的内存模型(第21讲还会详解),程序在运⾏时,局部变量将会存放在虚拟机栈中,⽽共享变量将会被保存在堆内存中。
加餐讲什么是数据的强、弱一致性 - 图5
由于局部变量是跟随线程的创建⽽创建,线程的销毁⽽销毁,所以存放在栈中,由上图我们可知,Java栈数据不是所有线程共享的,所以不需要关⼼其数据的⼀致性。

共享变量存储在堆内存或⽅法区中,由上图可知,堆内存和⽅法区的数据是线程共享的。⽽堆内存中的共享变量在被不同线程操作时,会被加载到⾃⼰的⼯作内存中,也就是CPU中的⾼速缓存。

CPU 缓存可以分为⼀级缓存(L1)、⼆级缓存(L2)和三级缓存(L3),每⼀级缓存中所储存的全部数据都是下⼀级缓存的
⼀部分。当 CPU 要读取⼀个缓存数据时,⾸先会从⼀级缓存中查找;如果没有找到,再从⼆级缓存中查找;如果还是没有找到,就从三级缓存或内存中查找。

如果是单核CPU运⾏多线程,多个线程同时访问进程中的共享数据,CPU 将共享变量加载到⾼速缓存后,不同线程在访问缓存数据的时候,都会映射到相同的缓存位置,这样即使发⽣线程的切换,缓存仍然不会失效。

如果是多核CPU运⾏多线程,每个核都有⼀个 L1缓存,如果多个线程运⾏在不同的内核上访问共享变量时,每个内核的L1缓存将会缓存⼀份共享变量。

假设线程A操作CPU从堆内存中获取⼀个缓存数据,此时堆内存中的缓存数据值为0,该缓存数据会被加载到L1缓存中,在操作后,缓存数据的值变为1,然后刷新到堆内存中。

在正好刷新到堆内存中之前,⼜有另外⼀个线程B将堆内存中为0的缓存数据加载到了另外⼀个内核的L1缓存中,此时线程A将堆内存中的数据刷新到了1,⽽线程B实际拿到的缓存数据的值为0。

此时,内核缓存中的数据和堆内存中的数据就不⼀致了,且线程B在刷新缓存到堆内存中的时候也将覆盖线程A中修改的数据。这时就产⽣了数据不⼀致的问题。
加餐讲什么是数据的强、弱一致性 - 图6
了解完内存模型之后,结合以上解释,我们就可以回过头来看看第⼀段代码中的运⾏结果是如何产⽣的了。看到这⾥,相信你可以理解图中1,1的运⾏结果了。

加餐讲什么是数据的强、弱一致性 - 图7

重排序

除此之外,在Java内存模型中,还存在重排序的问题。请看以下代码:

//代码1
public class Example { int x = 0;
boolean flag = false; public void writer() {
x = 1; //1
flag = true; //2
}

public void reader() {
if (flag) { //3 int r1 = x; //4
System.out.println(r1==x)
}
}
}
加餐讲什么是数据的强、弱一致性 - 图8

如果两个线程同时运⾏,线程2中的变量的值可能会出现以下两种可能:
加餐讲什么是数据的强、弱一致性 - 图9
现在⼀起来看看 r1=1 的运⾏结果,如下图所示:

加餐讲什么是数据的强、弱一致性 - 图10

那r1=0⼜是怎么获取的呢?我们再来看⼀个时序图:

加餐讲什么是数据的强、弱一致性 - 图11

在不影响运算结果的前提下,编译器有可能会改变顺序代码的指令执⾏顺序,特别是在⼀些可以优化的场景。

例如,在以下案例中,编译器为了尽可能地减少寄存器的读取、存储次数,会充分复⽤寄存器的存储值。如果没有进⾏重排序优化,正常的执⾏顺序是步骤1\2\3,⽽在编译期间进⾏了重排序优化之后,执⾏的步骤有可能就变成了步骤1/3/2或者2/1/3, 这样就能减少⼀次寄存器的存取次数。



int x = 1;//步骤1:加载x变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写⼊到寄存器指定的内存中
boolean flag = true; //步骤2 加载flag变量的内存地址到寄存器中,加载true到寄存器中,CPU通过mov指令把1写⼊到寄存器指定的内存中
int y = x + 1;//步骤3 重新加载x变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写⼊到寄存器指定的内存中
加餐讲什么是数据的强、弱一致性 - 图12

在 JVM 中,重排序是⼗分重要的⼀环,特别是在并发编程中。可 JVM 要是能对它们进⾏任意排序的话,也可能会给并发编程带来⼀系列的问题,其中就包括了⼀致性的问题。

Happens-before规则

为了解决这个问题,Java提出了Happens-before规则来规范线程的执⾏顺序:

程序次序规则:在单线程中,代码的执⾏是有序的,虽然可能会存在运⾏指令的重排序,但最终执⾏的结果和顺序执⾏的
结果是⼀致的;
锁定规则:⼀个锁处于被⼀个线程锁定占⽤状态,那么只有当这个线程释放锁之后,其它线程才能再次获取锁操作;
volatile变量规则:如果⼀个线程正在写volatile变量,其它线程读取该变量会发⽣在写⼊之后; 线程启动规则:Thread对象的start()⽅法先⾏发⽣于此线程的其它每⼀个动作;
线程终结规则:线程中的所有操作都先⾏发⽣于对此线程的终⽌检测;
对象终结规则:⼀个对象的初始化完成先⾏发⽣于它的finalize()⽅法的开始;
传递性:如果操作A happens-before 操作B,操作B happens-before操作C,那么操作A happens-before 操作C; 线程中断规则:对线程interrupt()⽅法的调⽤先⾏发⽣于被中断线程的代码检测到中断事件的发⽣。

结合这些规则,我们可以将⼀致性分为以下⼏个级别:

严格⼀致性(强⼀致性):所有的读写操作都按照全局时钟下的顺序执⾏,且任何时刻线程读取到的缓存数据都是⼀样的,Hashtable就是严格⼀致性;
加餐讲什么是数据的强、弱一致性 - 图13
顺序⼀致性:多个线程的整体执⾏可能是⽆序的,但对于单个线程⽽⾔执⾏是有序的,要保证任何⼀次读都能读到最近⼀次写
⼊的数据,volatile可以阻⽌指令重排序,所以修饰的变量的程序属于顺序⼀致性;
加餐讲什么是数据的强、弱一致性 - 图14
弱⼀致性:不能保证任何⼀次读都能读到最近⼀次写⼊的数据,但能保证最终可以读到写⼊的数据,单个写锁+⽆锁读,就是弱⼀致性的⼀种实现。

今天的加餐到这⾥就结束了,如有疑问,欢迎留⾔给我。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他
⼀起学习。

加餐讲什么是数据的强、弱一致性 - 图15

  1. 精选留⾔

加餐讲什么是数据的强、弱一致性 - 图16Liam
⽼师好,请教⼀个问题:

⽂中举例,数据不⼀致是多核CPU的⾼速缓存不⼀致导致的,是否意味着单核CPU多线程操作就不会发⽣数据不⼀致呢
2019-07-06 08:14
作者回复
也会的,线程安全除了要保证可⻅性,还需要保证原⼦性、有序性。
2019-07-07 09:56

加餐讲什么是数据的强、弱一致性 - 图17Lost In The
Echo。
⽼师,请问强⼀致性和顺序⼀致性有什么区别吗?
2019-07-06 20:43
作者回复
顺序⼀致性是指单个线程的执⾏的顺序性,强⼀致性则指的是多个线程在全局时钟下的执⾏的顺序性。
2019-07-07 09:42

加餐讲什么是数据的强、弱一致性 - 图18东⽅奇骥
上⾯例⼦,flag加volatile修饰,根据happens before中的顺序性选择和volatile的原则,就能保证另⼀个线程读到写⼊的值了。
2019-07-06 21:28
作者回复
对的,volatile除了可以保证变量的可⻅性,可以阻⽌局部指令重排序。
2019-07-07 09:40

加餐讲什么是数据的强、弱一致性 - 图19⻘梅煮酒
⽼师,请问⼀下,每核CPU都有⾃⼰的L1和L2,那么L1和L2的主要区别是什么呢?为什么不能合到⼀起呢?
2019-07-16 19:00
作者回复
L1\L2\L3三个缓存的作⽤和实现的技术是不⼀样的,L1的内存⼤⼩是⾮常有限的,所以很多时候在L1获取缓存数据的命中率⾮常低。为了提⾼CPU读取的速率,在L1没有命中的缓存,可以进⼊到L2进⾏获取,L2的容量要⽐L1⼤,但离CPU核⼼更远。但还是能提⾼CPU读取缓存数据的速率。
2019-07-17 09:34

加餐讲什么是数据的强、弱一致性 - 图20明翼
早看到就好了 ,⽼师请教下这么多知识点你是怎么记住的?
2019-07-10 06:22
作者回复
平时善于做笔记,除此之外,尝试将⾃⼰学到的知识点分享给其他⼈。
2019-07-10 09:31

加餐讲什么是数据的强、弱一致性 - 图21⾯朝⼤海
int x = 1;// 步骤 1:加载 x 变量的内存地址到寄存器中,加载 1 到寄存器中,CPU 通过 mov 指令把 1 写⼊到寄存器指定的内存中
boolean flag = true; // 步骤 2 加载 flag 变量的内存地址到寄存器中,加载 true 到寄存器中,CPU 通过 mov 指令把 1 写⼊到寄存器指定的内存中
int y = x + 1;// 步骤 3 重新加载 a 变量的内存地址到寄存器中,加载 1 到寄存器中,CPU 通过 mov 指令把 1 写⼊到寄存器指定的内存中

2019-07-08 08:38

加餐讲什么是数据的强、弱一致性 - 图22TWO STRINGS
⽼师您好,都说concurrenthashmap的get是弱⼀致性,但我不理解啊,volatile 修饰的变量读操作为什么会读不到最新的数据

2019-07-08 08:25
作者回复
我们知道Node以及Node的value是volatile修饰的,所以在⼀个线程对其进⾏修改后,另⼀个线程可以⻢上看到。 如果是⼀个新Node,那么就不能⻢上看到,虽然Node的数组table被volatile修饰,但是这样只是代表table的引⽤地址如果被修 改,其他线程可以⽴⻢看到,并不代表table⾥的数据被修改⽴⻢可以看到。
2019-07-08 16:53

加餐讲什么是数据的强、弱一致性 - 图23-W.LI-
⽼师好volatile+cas是强⼀致性么?。L1直接刷回主存,L2和L3需要做什么操作么?开头说每⼀级都是上⼀级的⼦集来着。
2019-07-07 16:12
作者回复
cas+volatile可以解决单个变量的强⼀致性问题。
2019-07-08 17:04

加餐讲什么是数据的强、弱一致性 - 图24云封
⽼师,请问下,如果不存在操作共享变量的情况或者把共享产量存在redis中,多线程结果就不会发⽣由于指令重排⽽导致结果不⼀致的情况。
2019-07-07 12:10
作者回复
指令重排序不⼀定是由于共享变量导致的,这块需要结合具体的场景分析。
2019-07-08 17:07

加餐讲什么是数据的强、弱一致性 - 图25Jxin
请问⽼师,指令重排优化会受多线程影响吗?感觉应该不会出现赋值为true和x=1这两条指令对换位置。因为从单线程来看这没 有指令重排的价值,所以感觉不会做重排优化。⽽如果重排优化会受多线程影响,那么场景1的r1==1应该是赋值为true,然后进⼊了if逻辑,接着优先执⾏x=1才导致的r1==1的结果。布尔赋值为true和if判断应该要紧挨着,减少⼀次寄存器加载该临时变量值。也就是⽼师那个场景1不会出现。
2019-07-06 17:06
作者回复
这⾥只是假设,有专⻔⼀个指令重排序的例⼦。
2019-07-07 09:44

加餐讲什么是数据的强、弱一致性 - 图26-W.LI-
⽼师容我问⼀个很基础的问题!⽗类private的属性会被⼦类继承么?⼦类创建的时候JVM给⼦类分配内存的时候,我看书上有说
⽗类的属性会排在⼦类前⾯有可能穿插。可是没写是否会给⼦类分配⽗类的私有属性内存空间。⼦类创建的时候,会默认调⽤
⽗类的⽆参构造器。这时候就会实例化⼀个⽗类对象么?(如果⽗类没有⽆参构造器会报错或者需要显示调⽤⽗类的有参构造器)
。如果每次实⼒⼦类对象的时候都会先创建⼀个⽗类对象的话,滥⽤继承。就会浪费很多内存是么?对象头就需要8字节了。
2019-07-06 12:22
加餐讲什么是数据的强、弱一致性 - 图27密码123456
单核也会有问题的,还有重排序。
2019-07-06 10:59
作者回复
会有重排序问题
2019-07-07 09:49

加餐讲什么是数据的强、弱一致性 - 图28nightmare
点赞666
2019-07-06 07:52