理解为什么需要内存屏障,双重检查锁是入门这个概念的经典。
一般发生在 对同一个对象的读写顺序没按照process order执行。e.g.
内存屏障主要保证了可见性,其实和我们提到的"线性"有一定重叠,同一时刻,每个线程应该看到执行操作的预期结果,而不是中间结果。
这里对obj同时存在读写,理论上,如果我们声明自己new Object(),其他线程就该拿到一个指向堆上对象的指针,并且这个堆上对象已经被构造好了。
但考虑下面的流程 1. 申请地址 2. 构造对象 3. 返回地址 如果2和3被reorder了,而且其他线程不小心看到了这个中间对象,那其他线程会拿到一个地址,指向了未构造好的对象。
所以,obj要设置为volatile的,保证两点
new Object()不会被重排序- obj这个局部对象应该立刻写回主存,不要一直在首次实例化对象的那个线程的本地缓存中。
if (obj != null) {
// 1. 申请一块地址
// 2. 在该地址上构造对象
// 3. 把内存地址给obj
obj = new Object();
}
概念解读自gpt
TSO和PO是两种内存序。我们并发编程用的内存序应该就是PO(如果不加以任何控制的话),不同语言提供的内存序保证也不同,比如java的 volatile,final内存序,c++也有类似的std::memory_order_release, std::memory_order_acquire 保证。
TSO应该是处理器级别的内存序保证
- 读读不会被reorder,写写不会被reorder
- 不同内存访问,读写可能reorder(比如编程顺序是先读A,后写B。由于A和B无关,执行顺序被cpu掉换成先写B,再读A)。
TSO(Total Store Order)和PO(Process Order)是计算机体系结构中关于内存操作顺序的两种不同模型。这些模型定义了在多处理器系统中,内存操作(如读取和写入)如何被顺序执行,以及它们对其他处理器的可见性。这些内存顺序模型对并发数据结构的设计和性能有重要影响。
### TSO(Total Store Order)
TSO是一种内存一致性模型,它要求所有的写入(存储操作)必须按照特定的全局顺序被所有处理器看到。在TSO模型中,处理器可以在本地缓存中缓存写入操作,但所有处理器最终都会按照相同的顺序观察到这些写入。
在TSO模型下,处理器可以在不影响全局存储顺序的情况下,对本地读取进行优化。这意味着一个处理器可以读取自己的写入,即使这些写入还没有被其他处理器看到。这种模型简化了编程模型,因为它提供了一种类似于单处理器系统的一致性视图。
### PO(Process Order)
PO是指每个单独处理器上的内存操作顺序。在单个处理器内,操作(包括内存操作和其他指令)都会按照程序中的顺序执行。这是一种非常直观的模型,因为它反映了程序员编写代码时的预期顺序。
在多处理器系统中,虽然每个处理器都遵循自己的PO,但是不同处理器上的操作可能会以不同的顺序交错执行。这就是为什么需要像TSO这样的内存一致性模型来定义不同处理器间操作的全局顺序。
### TSO和PO的区别和共同点
区别:
- TSO主要关注多处理器系统中全局的存储操作顺序,而PO关注单个处理器内部的操作顺序。
- TSO提供了一个关于写入操作如何被系统中所有处理器看到的全局一致性保证,而PO不涉及不同处理器之间的一致性问题。
共同点:
- 两者都是关于操作顺序的模型,确保了在某些约束下的可预测性。
### 对并发结构的影响
- **编程简化**:TSO提供了一种相对简单的编程模型,因为它保证了全局的存储顺序,这使得编写并发程序时可以做出某些假设,比如写入的顺序性。
- **性能优化**:TSO允许处理器进行某些优化,如本地缓存写入,这可能会提高性能,但也可能需要附加的同步指令(如内存屏障)来维持一致性。
- **同步机制**:在TSO模型下,构建并发数据结构时可能需要较少的同步机制,因为模型本身提供了一定程度的顺序保证。而在更松散的一致性模型下,可能需要更复杂的同步机制来保证数据的一致性。
了解系统的内存顺序模型对于构建正确和高效的并发数据结构至关重要,因为它影响了同步原语的选择和数据结构的设计。
最后理解一下内存屏障的语义
- 针对不同变量的读写顺序: 我们称为reorder,内存屏障可以控制reorder行为
- 针对同一个变量的读写, 我们称为可见性。在TSO的保证下,同一个变量的读写不会被reorder, 这时候就要考虑可见性了。
reorder和可见性并不是互相依赖的关系。
同时,reorder和可见性在程序级别是可控的,不同语言有自己的内存序。本质上是在汇编码里插入对CPU执行的hint。
处理器本身也能保证一定的内存屏障,可以理解处理器对于一些明显不需要reorder的case会自觉不去reorder它。如果处理器不管啥case都可能reorder,就说他提供一种"弱的一致性"保证。TSO表示"处理器自觉不对相同变量的读写序进行reorder",是一种比较强的保证。
reorder分为: 不同变量的读写顺序,同一变量的读写顺序。
可见性表示当前线程写一个东西,一般是写在本地缓存的,如果当前线程比较有素质,会把这个修改写回,那么其他线程就能看到这个修改。因此 重排序和可见性是两个维度的事。
重排序主要是对 线程内而言,可见性则是线程间,内存序是一个全局视角,需要同时保证线程内+线程间满足。
思考问题时,我们可以从几点来看
(1) 同一个线程,对同一个变量的读写顺序。
(2) 同一个线程,对不同变量的读写顺序
(3) 线程B能不能读到线程A对同一个变量的写入
- 处理器可能会对对任何指令流重排序 + 处理器不保证可见性:
那么,如果编程顺序是 P1 先读A,再写A。处理器可能交换顺序,导致P1读到一个更新的A值。
这个可以用 x, y 然后 if (x) write y, if (y) write x 这样的程序来看。
同时,如果P1写入了A(ver3), P2可能要等一会才能读到A(ver3),因为P1对A的修改在本地缓存,此时内存A的版本为ver2。
所以,在这个条件下,线程内和线程间都没法保证指令严格按照编程顺序来,一不小心就会死锁。
- 处理器对同一个变量的读写顺序不重排 + 不保证可见性:
那么,如果编程顺序是 P1 先读A,再写A。此时不会交换顺序这就保证,线程内对同一个变量的读写顺序是预期的(满足源代码级别的happens-before)
如果编程顺序是 P1 先读X,再写Y,再读Y,再写X。我们发现一定有
读X 发生在 写X之前, 写Y发生在读Y 之前 (同一个变量相对顺序不变。)
但是,实际执行顺序可能变成 先读X,再写X。再写Y,再读Y,
这个顺序依然满足 读X 发生在 写X之前, 写Y发生在读Y 之前,但我们发现,其实处理器交换了写X和写Y的顺序,其原因是两个写,写的是不同的变量。
解决办法就是加一个写写屏障: 先读X,再写Y,StoreStore, 再读Y,再写X
这个StoreStore表示: 只要是写写(不管写的是不是同一个),都不能交换。
但是,P1写入的A,不会第一时刻被其他线程看到,尤其是CLH Lock这种 当前线程盯其他线程内存的case,就可能会有未预期的结果。
当然不能依赖处理器能给我们什么,我们给通过编程语言提供的内存序保证,对处理器进行hint,具体的随便找个JMM/C++内存模型看看就行