CPU内部也会有自己的缓存,内部的缓存单位是行,叫做缓存行。在多核环境下会出现CPU之间的内存同步问题(比如一个核加载了一份缓存,另外一个核也要用到同一份数据),如果每个核每次需要时都往内存中存取,这会带来比较大的性能损耗,这个问题一般是通过MESI协议来解决的。
MESI协议中包含M、E、S、I四个状态,分别的意思是:
- M(修改, Modified): 本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此cache只有本地一个拷贝(专有).
- E(专有, Exclusive): 缓存行内容和内存中的一样, 而且其它处理器都没有这行数据
- S(共享, Shared): 缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝
- I(无效, Invalid): 缓存行失效, 不能使用
cpu在对缓存行进行了不同的操作后,在cpu缓存行中会记录缓存的不同状态。当一个核要对共享的数据进行写操作时,需要给其他核发送RFO(REQUEST FOR OWNER)消息并把其他核的数据改成I态。这是一种比较消耗性能的操作。
cpu的伪共享问题本质是:几个在逻辑上并不包含在同一个内存单元内的数据,由于被cpu加载在同一个缓存行当中,当在多线程环境下,被不同的cpu执行,导致缓存行失效而引起的大量的缓存命中率降低。
例如:当两个线程分别对一个数组中的两份数据进行写操作,每个线程操作不同index上的数据,看上去,两份数据之间是不存在同步问题的,但是,由于他们可能在同一个cpu缓存行当中,这就会使这一份缓存行出现大量的缓存失效,如前所述当一份线程更新时要给另一份线程发送RFO消息并把它的缓存失效掉。
解决这个问题的一个办法是让这个数组中不同index的数据在不同的缓存行:因为缓存行的大小是64个字节,那我们只要让数组中没份数据的大小大于64个字节,就可以保证他们在不同的缓存行当中,就能避免这样的伪共享问题。
比如一个类当中原本只有一个long类型的属性。这样这个类型的对象只占了16个字节(java对象头有8字节),如果这个类型被定义成一个长度为4的数组,这个数组的所有数据都可能在一个缓存行当中,就可能出现伪共享问题,那么这个时候,就可以采用补齐(padding)的办法,在这个类型中加上public long a,b,c,d,e,f,g;这六个无用的属性定义,使得这个类型的一个实例占用内存达到64字节,这样这个类型的伪共享问题就得到了解决,在多线程当中对这个类型的数组进行写操作就能避免伪共享问题。