(一)volatile的作用
1:volatile的作用是保证共享变量的可见性,不能保证原子性,也不能保证线程安全。
2:volatile的作用是确保所有线程在同一时刻读取到的共享变量的值是一样的。
3:如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立刻看到这个更新。
(二)硬件系统架构演进
以下是三级缓存架构(高速缓存结构,CPU自带的位于每个核旁边有个单独的区域):L1->L2->L3(速度依次下降)
实际上:
电脑将字从硬盘加载到内存,然后CPU处理,下一步是将字回写到内存,然后再让打印机打印
CPU读取速度是快了,问题是:CPU操作完成后,如何数据写入?缓存分布在每个CPU中,如何保障写入后的数据与各个CPU之间缓存数据保持一致。
(三)缓存一致性问题:
单线程情况下是这样:
对于一个常量应该是存在常量池中
对于不同CPU中的不同线程执行同一个操作:
当程序在运行过程中,会将运算需要的数据复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从他的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
举个简单的例子,比如:i = i + 1;
当线程执行这个语句的时候,会先从主存中读取i的值,然后复制一份到高速缓存中,然后CPU执行指令对i进行+1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。就以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2.但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行+1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行+1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2.这就是缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决办法:
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面的例子中,如果一个线程在执行i = i + 1,如果在执行这段代码的过程中,在总线上发出了LOCK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
缓存一致性协议:
在多核CPU系统中,每个CPU核心都有自己的一级缓存,二级缓存等。这样一来当多个CPU核心在对共享的数据进行写操作时,就需要保证该共享数据在所有CPU核心中可见性/一致性。
【窥探技术+MESI协议】的出现,就是为了解决多核CPU问题,缓存一致性问题。
说明:E:独占。M:已修改。
所以就出现了缓存一致性协议。该协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU向内存写入数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通过总线(高速缓存区与内存之间的连接通道是所有线程和CPU核共用的)通知其他CPU将该变量的缓存行设置为无效状态,因此当其他CPU需要读取这个变量时,会发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。