volatile(1)
目录
1.volatile的作用
2.硬件系统架构
3.缓存一致性问题
4.缓存一致性协议
第1节 volatile的作用
1. volatile的作用是保证共享变量的可见性,不能保证原子性,也不能保证线程安全。
2. volatile的作用是确保所有线程在同一时刻读取到的共享变量的值是一致的。
3. 如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立刻看到这个更新。
第2节 硬件系统架构
计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。
程序运行的数据是存储在主内存(通常说的内存)中,这时就会有一个问题:
读写主内存中的数据没有CPU中执行指令的速度快,
如果任何的交互都需要与主内存打交道则会大大降低CPU的效率(拖CPU的后腿)。
解决问题的办法:
CPU高速缓存的诞生。
CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。
现代CPU为了提高访问数据的效率,在每个CPU核心上都会有多级容量小,
速度快的缓存(分别称之为L1 cache,L2 cache,多核心共享L3 cache等),
用于缓存常用的数据。
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,
一般为 32-256 个字节。最常见的缓存行大小是 64个字节。
因此当CPU在执行一条读内存指令时,
它是会将内存地址所在的缓存行大小的内容都加载进缓存中的。
也就是说,一次加载一整个缓存行。
CPU首先使用自己的寄存器,然后使用速度更快的L1缓存,其中:
L1D缓存数据;
L1I缓存指令;
L1缓存和次快的L2做同步数据;L2缓存和L3缓存做同步数据(L2和L3按照内核数量做了等分,分给各个内核使用),L3和主内存同步数据。
CPU避免了与主内存直接打交道,与速度比主内存高出很多的CPU高速缓冲区打交道,充分利用了CPU的高性能。
CPU读取速度是够快了,问题是:CPU操作完成后,如何将数据写入?
缓存分布在每个CPU中,如何保障写入后的数据与各个CPU之间的缓存保持数据一致?
1. 直写(write-through)
直写是透过本级缓存,直接把数据写到下一级缓存(或直接到内存)中,如果对应的数据被缓存了,我们同时更新缓存中的内容(甚至直接丢弃)。所以,直写时缓存行永远和它对应的内存内容匹配。
2. 回写(write-back)
缓存不会立即把写操作传递到下一级,而是仅修改本级缓存中的数据,并且把对应的缓存数据标记为“脏”数据。脏数据会触发回写,也就是把里面的内容写到对应的内存或下一级缓存中。回写后,脏数据又变“干净”了。当一个脏数据被丢弃的时候,总是先要进行一次回写。
第3节 缓存一致性问题
举个栗子
i++;
当线程运行这段代码时
1. 首先会从主内存中读取i( i = 1)。
2. 然后复制一份到CPU高速缓存中,然后CPU执行 + 1 的操作。
3. 然后将数据+1后的结果写入到高速缓存中。
4. 最后将结果刷新到主存中。
其实这样做在单线程中是没有问题的,在多线程中是有问题。
假如有两个线程A、B都执行这个操作(i++),
按照我们正常的逻辑思维主内存中的i值应该是3,但事实是这样么?分析如下:
1. A、B两个线程从主存中读取i的值1到各自的高速缓存中。
2. 然后线程A执行+1操作并将结果写入高速缓存中,最后写入主内存中。
3. 此时主内存i==2,线程B做同样的操作,主内存中的i的值又被更新成2。
4. 所以A、B两个线程执行完毕后,最终结果为2并不是3。与我们预期值不等。
这种现象就是缓存一致性问题。
第4节 缓存一致性协议
解决缓存一致性方案有两种:
1.通过在总线加LOCK#锁的方式
2.通过缓存一致性协议
方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
在多核CPU系统中,每个CPU核心都有自己的一级缓存、二级缓存等。这样一来当多个CPU核心在对共享的数据进行写操作时,就需要保证该共享数据在所有CPU核心中的可见性/一致性。
【窥探技术 + MESI协议】的出现,就是为了解决多核CPU时代,缓存不一致的问题的。
“窥探”背后的基本思想是,所有内存传输都发生在一条共享的总线上,所有的CPU都能看到这条总线。
缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(arbitrate):同一个指令周期中,只有一个缓存可以读写内存。
窥探技术的思想是,缓存不仅仅在做内存传输的时候才和总线打交道,而是不停地在窥探总线上发生的数据交换,跟踪其他缓存在做什么。
所以当一个缓存代表它所属的CPU去读写内存时,其他CPU都会得到通知,它们以此来使自己的缓存保持同步。只要某个CPU一写内存,其他CPU马上就知道这块内存在它们自己的缓存中对应的缓存行已经失效。
缓存系统操作的最小单位就是缓存行,而MESI是缓存行四种状态的首字母缩写,任何多核系统中的缓存行都处于这四种状态之一。
① 失效(Invalid)缓存行:该CPU缓存中无该缓存行,或缓存中的缓存行已经失效了。
② 共享(Shared)缓存行:缓存行的内容是同主内存内容保持一致的一份拷贝,在这种状态下的缓存行只能被读取,不能被写入。多组缓存可以同时拥有针对同一内存地址的共享缓存行。
③ 独占(Exclusive)缓存行:和S状态一样,也是和主内存内容保持一致的一份拷贝。
区别在于,如果一个CPU持有了某个E状态的缓存行,那其他CPU就不能同时持该内容的缓存行,所以叫“独占”。
这意味着,如果其他CPU原本也持有同一缓存行,那么它会马上变成“失效”状态(I状态)。
④ 已修改(Modified)缓存行:该缓存行已经被所属的CPU修改了。
如果一个缓存行处于已修改状态,那么它在其他CPU缓存中的拷贝马上会变成失效状态。
此外,已修改缓存行如果被丢弃或标记为失效(即,从M状态 ——> I状态),那么先要把它的内容回写到内存中 ———— 这和回写模式下常规的处理方式一样。
只有当缓存行处于E或M状态时,CPU才能去写它,也就是说只有这两种状态下,CPU是独占这个缓存行的。
当CPU想写某个缓存时,如果它没有独占权,它必须先发送一条“我要独占权”的请求给总线,这会通知其他CPU,把它们拥有的同一缓存行的拷贝失效(I状态)。
只有在获得独占权后,CPU才能开始修改数据。并且此时,这个CPU知道,这个缓存行只有一份拷贝,在我自己的缓存里,所以不会有任何冲突。
反之,如果有其他CPU想读取这个缓存行(我们马上能知道,因为我们一直在窥探总线),独占或已修改的缓存行必须先回到“共享”状态。