1、计算机数据存放

多线程java保持事务一致性_java

 

多线程java保持事务一致性_数据_02

 

CPU的三级缓存,L1,L2缓存为CPU单核独享,L3为多核共享

2、为什么这样设计

因为CPU的速度要远远大于内存的速度,为了解决这个问题,CPU引入了三级缓存:L1,L2和L3三个级别,L1最靠近CPU,L2次之,L3离CPU最远,L3之后才是主存。速度是L1>L2>L3>主存,越靠近CPU的容量越小。CPU获取数据会依次从三级缓存中查找,如果找不到再从主存中加载。

多线程java保持事务一致性_主存_03

当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个CPU缓存架构中最为重要的部分。

3、带来的问题

L1,L2,L3和内存中的数据可能不同,造成数据不一致。

4、解决方法

(一)、共用一个缓存

这种方式就类似于将一个多线程问题,通过同步的方式解决一样,公用一个缓存,那么在某个核心在使用该数据的时候,其他的核心将会被阻塞等待,这样也会降低CPU的效率。

(二)、在总线(bus)上加lock锁的方式

这个是在不修改对应硬件结构上,来让DRAM的数据只能由1个核心加载到自己缓存中。我们从前面CPU的工作原理,大概可以得知CPU从RAM获取数据基本都是通过将CPU和RAM通过总线进行连通来通信的。这种方式和上面一样不好,锁住总线会让其他和其不想干的CPU都无法访问RAM,相当于一个悲观锁,效率比较低下。

(三)、缓存一致性协议的方式 MESI

通过锁定缓存行的方式来来保证多核CPU和内存读写的一致性问题。其方案类似于读写锁的方式,使得针对同一地址的读内存操作是并发的,而针对同一地址的写内存操作是独占的。

5、MESI协议

(一)、缓存行状态

CPU的缓存是以缓存行(cache line)为单位的,MESI协议描述了多核处理器中一个缓存行的状态。在MESI协议中,每个缓存行有4个状态,分别是:

  • M(修改,Modified):本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有);
  • E(专有,Exclusive):缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
  • S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝;
  • I(无效,Invalid):缓存行失效, 不能使用

缓存行的E状态如下图:

多线程java保持事务一致性_缓存_04

此时只有core1访问缓存行,它的缓存行的状态为E,表示core1独占。

缓存行的S状态如下图:

多线程java保持事务一致性_缓存_05

此时core1和core2都会访问缓存行,他们的缓存行状态为S,表示缓存行处于共享状态

缓存行的M和I状态如下图:

多线程java保持事务一致性_多线程java保持事务一致性_06

此时core1修改了缓存行,因此core1的缓存行状态为M,代表已经修改,而core2的缓存行状态为I,代表已经失效,需要从主存中读取

(二)、缓存监听任务

状态

描述

监听任务

Modified(修改)

该缓存行有效,但是该缓存数据已经被当前核心修改,此时和DRAM中数据不一致。我们将其置为M,其他的核中缓存行都会置为I。

监听总线上所有对该缓存行写回DRAM的操作(不希望别人写入),需要将该操作延迟到自己将缓存行写回到主存后变成S状态。

Exclusive(互斥)

该缓存行有效,数据和RAM的数据一致,数据只存在当前内核工作内存中,只有他在使用是独占的。

监听总线上所有从DRAM读取该缓存行的操作,一旦有读的,需要将状态置为S状态。

Shared(共享)

该缓存行有效,不过当前缓存行在多个核中都有,并且大家以及DRAM中的都一样

监听其他的缓存中将该缓存置为I或者为E的事件,将状态置为I状态。

Invalid(无效)

表明该缓存行无效,如果想要获取数据的话,就去DRAM中加载最新的

不需要监听。

(三)、缓存行状态转换

在MESI协议中,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移。MESI协议状态迁移图如下:

多线程java保持事务一致性_缓存_07

  • 初始:一开始时,缓存行没有加载任何数据,所以它处于 I 状态。
  • 本地写(Local Write):如果本地处理器写数据至处于 I 状态的缓存行,则缓存行的状态变成 M。
  • 本地读(Local Read):如果本地处理器读取处于 I 状态的缓存行,很明显此缓存没有数据给它。此时分两种情况:(1)其它处理器的缓存里也没有此行数据,则从内存加载数据到此缓存行后,再将它设成 E 状态,表示只有我一家有这条数据,其它处理器都没有;(2)其它处理器的缓存有此行数据,则将此缓存行的状态设为 S 状态。(备注:如果处于M状态的缓存行,再由本地处理器写入/读出,状态是不会改变的)
  • 远程读(Remote Read):假设我们有两个处理器 c1 和 c2,如果 c2 需要读另外一个处理器 c1 的缓存行内容,c1 需要把它缓存行的内容通过内存控制器 (Memory Controller) 发送给 c2,c2 接到后将相应的缓存行状态设为 S。在设置之前,内存也得从总线上得到这份数据并保存。
  • 远程写(Remote Write):其实确切地说不是远程写,而是 c2 得到 c1 的数据后,不是为了读,而是为了写。也算是本地写,只是 c1 也拥有这份数据的拷贝,这该怎么办呢?c2 将发出一个 RFO (Request For Owner) 请求,它需要拥有这行数据的权限,其它处理器的相应缓存行设为 I,除了它自已,谁不能动这行数据。这保证了数据的安全,同时处理 RFO 请求以及设置I的过程将给写操作带来很大的性能消耗

6、BUS总线监听缓存行修改

  • 当某个核心修改了数据,状态变更M(已修改)时,总线监听到缓存变更,修改内存状态为I(已失效),并发出当前缓存失效的通知,所有监听当前缓存地址的线程接收到通知后,把当前缓存状态修改为I(已失效,表示使用时需要重新从内存中加载)。
  • 当前线程把更新的数据写回内存数据非常快。
  • 当前内存数据的变更,先加Lock指令,原子级变更,变更内存状态为S(可以使用)。
  • 其余线程在使用时,如果内存状态为S,表示可以使用,如果还是I(是否线程等待不确定,理论上应该是等待变为S)。

   总结:MESI保证了寄存器,L1,L2,L3,内存数据的一致性;     

7、多线程共享变量线程安全处理

  • 使用synchronized关键字,它可以是类锁和对象锁,当加了关键字时,就会添加一个monitor对象,对象锁,monitor监视器会添加给对象,类锁,monitor会添加给类,这也解释了为什么有些时候添加了synchronized,线程也不能锁住,因为添加的可能是对象锁,不同的对象获取的monitor监视器是不同的,所以不会锁住,它有2个指令monitorenter和monitorexit指令,当线程代码执行到这个monitor的地方的时候,它会去尝试获取monitor,当它获取到monitor的时候才可以执行monitorenter指令(其实就是一个计数器,当计数器为0的时候,可以执行monitorenter,之后计数器加1),退出会执行monitorexit指令(这时候计数器会减1),并放弃锁,获取锁和释放锁都由JVM完成,操作简单,但获取锁和释放锁对CPU性能使用会过多,所以synchronized是一个重量级锁。
  • Lock,通过 lock.lock()加锁,lock.unLock()解锁,lock锁使用的是CAS原理实现的锁,它维护了一个锁当前的状态信息和等待锁的队列,当线程处于可以加锁的状态时,当前线程执行加锁操作,并修改线程状态,之后执行逻辑代码,当线程为执行释放锁是,另一个线程执行到加锁的代码,不能获取锁,就会把这个线程放入一个队列中,直到持有锁的线程执行unLock方法释放锁,之后,线程进行竞争锁,使用CAS原理,竞争到锁的线程继续上面的逻辑。
  • java的concurrent包下面的原子类,通过volatile和CAS自旋保证线程安全,volatile只能保证可见性和防止指令重排序,CAS自旋保证原子性,实现线程安全。
  • 通过ThreadLocal实现线程副本数据隔离,实现线程安全。
  • 通过业务代码实现多线程(如通过consistentHash取值,分开业务数据)。
  • Condition绑定在Lock锁上使用(代替Object的wait,notify,notifyAll方法)。
  • CountDownLatch,可以用于判断线程是否都已经执行完成,没有执行完成,可以让让线程暂停(Condition和CountDownLatch底层都是用CAS原理)。

8、CAS自旋的原理

多线程java保持事务一致性_主存_08

多线程java保持事务一致性_java_09

  • CAS原理:Compare and swap,比较并设置,当需要设置新值,会把初始值也传过去,当初始值相等时,才会做更新操作,否则循环执行。
  • CAS的ABA问题:原始值为1,线程A修改为2,线程B又修改为1,线程C认为数据没有改变,直接修改为3,导致中间的过程变更不知道,不影响实际业务可以不做处理。
  • 当数据出现多个线程修改同一个值的时候,cas重试次数会过多,反而降低性能,这时候建议用synchronized,1.6之后的synchronized效率并不差。