这里咱们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层缘由。java
随着CPU的发展,而由于CPU的速度和内存速度不匹配的问题(CPU寄存器的访问速度很是快,而内存访问速度相对偏慢),全部在CPU和内存之间出现了多级高速缓存。下图是现代CPU和内存的通常架构图:
咱们能够看到高速缓存也分为三级缓存,越靠近寄存器的级别缓存访问速度越快。其中L3 Cache为多核共享的,L1和L2 Cache为单核独享,而L1又有数据缓存(L1 d)和指令缓存(L1 i)。程序员
正由于高速缓存的出现,各CPU内核从主内存获取相同的数据将会存在于缓存中,当多核都对此数据进行操做并修改值,此时另外的核心并不知道此值已被其余核心修改,从而出现缓存不一致的问题。编程
解决缓存一致性问题通常有两个方法:segmentfault
为了解决缓存一致性的问题,一些CPU系列(好比Intel奔腾系列)采用了MESI协议来解决缓存一致性问题。此协议将每一个缓存行(Cache Line)使用4种状态进行标记。缓存
该缓存行只被缓存在该CPU核心的缓存中,而且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存须要在将来的某个时间点(容许其它CPU读取请主存中相应内存以前)写回(write back)主存。当被写回主存以后,该缓存行的状态会变成独享(exclusive)状态。安全
该缓存行只被缓存在该CPU核心缓存中,它是未被修改过的(clean),与主存中数据一致。该状态能够在任什么时候刻当有其它CPU核心读取该内存时变成共享状态(shared)。一样地,当CPU核心修改该缓存行中内容时,该状态能够变成Modified状态。多线程
该状态意味着该缓存行可能被多个CPU缓存,而且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行能够被做废(变成无效状态(Invalid))。架构
该缓存是无效的(可能有其它CPU核心修改了该缓存行)并发
在MESI协议中,每一个CPU核心的缓存控制器不只知道本身的操做(local read和local write),每一个核心的缓存控制器经过监听也知道其余CPU中cache的操做(remote read和remote write),再肯定本身cache中共享数据的状态是否须要调整。异步
针对操做,缓存行的状态迁移图以下:
在咱们编程过程当中,习惯性程序思惟认为程序是按咱们写的代码顺序执行的,举个例子来讲,某个程序中有三行代码:
int a = 1; // 1 int b = 2; // 2 int c = a + b; // 3
从程序员角度执行顺序应该是1 -> 2 -> 3,实际通过编译器和CPU的优化颇有可能执行顺序会变成 2 -> 1 -> 3(注意这样的优化重排并无改变最终的结果)。相似这种不影响单线程语义的乱序执行咱们称为指令重排。(后面讲Java内存模型也会讲到这部分。)
举个例子,咱们先看能够看一段代码:
class ReorderExample { int a = 0; boolean flag = false; public void write() { a = 1; // 1 flag = true; // 2 } public void read() { if (flag) { // 3 int i = a * a; // 4 } } }
在单线程的状况下若是先write再read的话,i的结果应该是1。可是在多线程的状况下,编译器极可能对指令进行重排,有可能出现的执行顺序是2 -> 3 -> 4 -> 1。这个时候的i的结果就是0了。(1和2之间以及3和4之间不存在数据依赖,有关数据依赖在后面的Java内存模型中会讲到。)
在CPU层面,一条指令被分为多个步骤来执行,每一个步骤会使用不一样的硬件(好比寄存器、存储器、算术逻辑单元等)。执行多个指令时采用流水线技术进行执行,以下示意图:
注意这里出现的”停顿“,出现这个缘由是由于步骤22须要步骤13获得结果后才能进行。CPU为了进通常优化:消除一些停顿,这时会将指令3(指令3对指令2和1都没有数据依赖)移到指令2以前进行运行。这样就出现了指令重排,根本缘由是为了优化指令的执行。
CPU通过长时间的优化,在寄存器和L1缓存之间添加了LoadBuffer、StoreBuffer来下降阻塞时间。LoadBuffer、StoreBuffer,合称排序缓冲(Memoryordering Buffers (MOB)),Load缓冲64长度,store缓冲36长度,Buffer与L1进行数据传输时,CPU无须等待。
由于StoreBuffer的存在,CPU在写数据时,真实数据并不会当即表现到内存中,因此对于其它CPU是不可见的;一样的道理,LoadBuffer中的请求也没法拿到其它CPU设置的最新数据;因为StoreBuffer和LoadBuffer是异步执行的,因此在外面看来,先写后读,仍是先读后写,没有严格的固定顺序。
因为引入StoreBuffer和LoadBuffer致使异步模式,从而致使内存数据的读写多是乱序的(也就是内存系统的重排序)。
为了解决CPU优化带来的不可见、重排序的问题,可使用内存屏障(memory barrier)来阻止必定的优化(在后面介绍Java内存模型也会详细结合讲内存屏障)。不一样的CPU架构对内存屏障的实现方式与实现程度很是不同,下面咱们看下X86架构中内存屏障的实现。
使全部Store Barrier以前发生的内存更新都是可见的。
使全部Store Barrier以前发生的内存更新,对Load Barrier以后的load操做都是可见的。
全部Full Barrier以前发生的操做,对全部Full Barrier以后的操做都是可见的。
在程序咱们常说的三大性质:可见性、原子性、有序性。经过线程安全性深层缘由咱们能更好的理解这三大性质的根本性缘由。(可见性、原子性、有序性会在后面文章中进行详细讲解。)