False share翻译成伪共享确实有点令人苦恼
这些不同数据数据被不同线程中的任意一个修改之后,会导致整个缓存行失效,会导致其他线程更新缓存行,这就是低效的原因.
这样虽然使缓存利用变低,但是会减少缓存频繁失效的问题
L1,L2,L3
Java中一个long8字节,所以一个缓存行可以存8个long类型
@sun.misc.Contended
class MyLong {
volatile long value;
}
Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。
class Pointer {
volatile long x;
long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
无法从系统层面上通过工具来探测伪共享事件。其次,不同类型的计算机具有不同的微架构(如 32 位系统和 64 位系统的 java 对象所占自己数就不一样),如果设计到跨平台的设计,那就更难以把握了,一个确切的填充方案只适用于一个特定的操作系统。
貌似是intel的一个协议
代表四个状态Modifyied,Exclusive,Shared,Invalid
Exclusive:所有处理的缓存行都没有此行数据,从内存加载到此缓存行后设置成E状态,表示只此一家
Shared:其他处理器有此行数据,则设置为S
Invalid:缓存行没有加载任何数据
Modified:写数据到I状态的缓存行,则置为M状态(处于M态的缓存行,本地处理器继续读写状态不会改变)
远程读写(跨核访问缓存行),如果读就把此缓存行拷贝过去,然后置为S态,如果远程写需要RFO request,此时其他处理器缓存行设为I态,除了自己谁也不许动这行数据,RFO和I态设置会有大消耗
LinkedBlockingQueue它的last和head经常被不同线程修改,但却可能在同一个缓存行,某些java编译器就会补齐数据
GC可能导致数据在内存和对应的CPU缓存行的位置发生变化所以padding时候要注意
netty和grizzly的代码中的LinkedTransferQueue中都使用了PaddedAtomicReference
ConcurrentHashMap 里面的 size() 方法使用的是分段的思想来构造的,每个段使用的类是 CounterCell,它的类上就有 @sun.misc.Contended 注解。
计算机的cpu物理核数是同时可以并行的线程数量(cpu只能看到线程,线程是cpu调度分配的最小单位),由于超线程技术,实际上可以并行的线程数量通常是物理核数的两倍,这也是操作系统看到的核数。我们只care可以并行的线程数量,所以之后所说的核数是操作系统看到的核数,所指的核也是超线程技术之后的那个核(不是物理核)。
进程是操作系统资源分配(内存,显卡,磁盘)的最小单位,线程是执行调度(即cpu调度)的最小单位(cpu看到的都是线程而不是进程),一个进程可以有一个或多个线程,线程之间共享进程的资源,通过这样的范式,就可以减少进程的创建和销毁带来的代价,可以让进程少一点,保持相对稳定,不断去调度线程就好。如果计算机有多个cpu核,且计算机中的总的线程数量小于核数,那线程就可以并行运行在不同的核中,如果是单核多线程,那多线程之间就不是并行,而是并发,即为了均衡负载,cpu调度器会不断的在单核上切换不同的线程执行,但是我们说过,一个核只能运行一个线程,所以并发虽然让我们看起来不同线程之间的任务是并行执行的,但是实际上却由于增加了线程切换的开销使得代价更大了。如果是多核多线程,且线程数量大于核数,其中有些线程就会不断切换,并发执行,但实际上最大的并行数量还是当前这个进程中的核的数量,所以盲目增加线程数不仅不会让你的程序更快,反而会给你的程序增加额外的开销。
from https://zhuanlan.zhihu.com/p/82123111