❝并发编程基础原理,个人理解,如有错误请指正。推荐书籍《Java并发编程的艺术》
❞
JMM (内存模型)
内存模型
- 每个线程都有一个私有的本地内存,本地内存中存储了该线程使用的共享变量的副本
- 本地内存只是JMM的一个抽象,具体实现依赖于具体硬件(高速缓存,寄存器等)
- 线程不直接读写主内存的共享变量,而是直接操作本地内存中的副本
硬件架构(常见架构,英特尔等)
结合JMM和硬件架构
- 当线程1抢占了核1的时间片,就拥有了核1及核1的L1 L2及共享L3使用权
- 在抢占的时间片内,线程1执行过程中若需要访问共享变量,就会去高速缓存中查找,若缓存miss则从主内存(也有可能从其他核的L1L2中查找)加载(此处包含预加载,为什么要有预加载见局部性原理)
- 通常所说的线程上下文切换过程中性能损耗是指第2步中缓存miss后的操作
- 若高速缓存中的副本被线程修改,cpu自行决定何时(一般是cpu空闲时)把修改后的值回写到主内存
- 若遇到Lock指令,cpu会强制回写主内存
- 步骤4,5中的回写主内存操作会使在其他缓存了该内存地址的数据(整个缓存行cacheLine)无效
- 当其他线程抢占任一核的时间片后,若高速缓存中的副本失效,会从主内存中重新加载共享变量
- 高速缓存和主内存一致性同步参考MESI一致性协议
volatile
顺序性
可见性
可见性定义
❝可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
❞
JVM如何实现volatile的可见性
- JVM解释执行或者编译执行时,若遇到共享变量的写操作会强制追加Lock指令
- 之后的过程见上面的步骤5,6,7
伪共享(并发编程的性能杀手)
伪共享描述
❝综合以上所述:缓存行容量为64字节(通常是,也有32的),缓存行中可以存放多个变量的副本(eg:8个long型),缓存行中任意一个变量被回写到主内存都会引起整个缓存行失效,无形中影响到了其他无竞争关系线程读取其他变量副本的成本。 若这些变量中有一个被volatile修饰,那么该变量的写操作必定触发回写主内存,必定会使其他变量的副本失效, 所以volatile要慎用
❞
如何解决
- padding
public class Test {
private volatile Long id;
/**
* 填充字段
*/
private Long p0, p1, p2, p3, p4, p5;
/**
* 阻止Jvm的无用字段消除优化
*/
public Long preventToBeEliminated() {
return p0 + p1 + p2 + p3 + p4 + p5;
}
}
@sun.misc.Contended该注解实际上也是填充,只不过比方法1更智能但只适用于java8及以上
–XX:+PrintFieldLayout -XX:-RestrictContended
public class Test {
private String age;
private String name;
/**
* Contended注解可以将id移动到远离其他字段的地方
*/
@Contended
private volatile Long id;
}
局部性原理(扩展)
- 时间局部性
- 空间局部性
- 分支局部性
- 等等
空间局部性
❝如果某个位置的信息被访问,那和它相邻的信息也很有可能被访问到。 这个很好理解,程序代码中有很多循环遍历数组等操作。 不仅限于数组,线性(分布是连续的)数据结构都可以,比如Java对象的各个字段在堆中就是连续的内存块分布(详见Java对象的内存布局)
❞
空间局部性的应用
主内存缓存硬盘 page cache :kafka,mysql等
mysql某个索引(索引在b+树叶子节点也是线性分布的)被命中后会一同加载该索引前后多条记录到同一个pageCache中, 这样可以减少磁盘IO次数,提升查询性能。kafka的消息log在磁盘中也是按分区内的消息顺序追加的,也可以很好的利用该特性
cpu高速缓存主内存 cache line:redis等
redis中的数据结构散列集Hash,当key数量小于一定值时散列集会被压缩成数组就是为了利用cpu高速缓存,这也是为什么使用Hash比使用多个KV要快的原因。详见redis官网-内存优化
MESI缓存一致性协议(扩展)
M(修改,Modified)
本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有)
E(专有,Exclusive)
缓存行内容和内存中的一样,而且其它处理器都没有这行数据
S(共享,Shared)
缓存行内容和内存中的一样,有可能其它处理器也存在此缓存行x拷贝
I(无效,Invalid)
缓存行失效, 不能使用