并发编程基础原理

用户头像
刚刚🏂
关注
发布于: 2020 年 07 月 31 日

并发编程基础原理,个人理解,如有错误请指正。推荐书籍《Java并发编程的艺术》

JMM (内存模型)

内存模型

  1. 每个线程都有一个私有的本地内存,本地内存中存储了该线程使用的共享变量的副本

  2. 本地内存只是JMM的一个抽象,具体实现依赖于具体硬件(高速缓存,寄存器等)

  3. 线程不直接读写主内存的共享变量,而是直接操作本地内存中的副本

硬件架构(常见架构,英特尔等)

结合JMM和硬件架构

  1. 当线程1抢占了核1的时间片,就拥有了核1及核1的L1 L2及共享L3使用权

  2. 在抢占的时间片内,线程1执行过程中若需要访问共享变量,就会去高速缓存中查找,若缓存miss则从主内存(也有可能从其他核的L1L2中查找)加载(此处包含预加载,为什么要有预加载见局部性原理)

  3. 通常所说的线程上下文切换过程中性能损耗是指第2步中缓存miss后的操作

  4. 若高速缓存中的副本被线程修改,cpu自行决定何时(一般是cpu空闲时)把修改后的值回写到主内存

  5. 若遇到Lock指令,cpu会强制回写主内存

  6. 步骤4,5中的回写主内存操作会使在其他缓存了该内存地址的数据(整个缓存行cacheLine)无效

  7. 当其他线程抢占任一核的时间片后,若高速缓存中的副本失效,会从主内存中重新加载共享变量

  8. 高速缓存和主内存一致性同步参考MESI一致性协议

volatile

顺序性

可见性

可见性定义

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

JVM如何实现volatile的可见性

  1. JVM解释执行或者编译执行时,若遇到共享变量的写操作会强制追加Lock指令

  2. 之后的过程见上面的步骤5,6,7

伪共享(并发编程的性能杀手)

伪共享描述

综合以上所述:缓存行容量为64字节(通常是,也有32的),缓存行中可以存放多个变量的副本(eg:8个long型),缓存行中任意一个变量被回写到主内存都会引起整个缓存行失效,无形中影响到了其他无竞争关系线程读取其他变量副本的成本。若这些变量中有一个被volatile修饰,那么该变量的写操作必定触发回写主内存,必定会使其他变量的副本失效,所以volatile要慎用

如何解决

  • padding ,eg:Disruptor

  • @sun.misc.Contended (java8及以上)

关于synchronized锁优化

锁升级不可逆,目的是为了提高获得锁和释放锁的效率。锁升级过程发生在运行期。编译期Jvm也会做一些优化:锁擦除,锁粒度调整

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。 —— 摘自《并发编程的艺术》

  • 优点:惰性解锁,竞争时才解锁,同一线程再次获取锁时无需再加锁

  • 缺点:偏向锁撤销需要等待全局安全点,暂停线程

轻量级锁

  • 自旋锁

  • 自适应自旋锁

重量级锁

  • 不自旋直接阻塞线程

AQS(J.U.C.基础)

AQS

Java并发之AQS详解

条件队列

局部性原理(扩展)

  1. 时间局部性

  2. 空间局部性

  3. 分支局部性

  4. 等等

空间局部性

如果某个位置的信息被访问,那和它相邻的信息也很有可能被访问到。 这个很好理解,程序代码中有很多循环遍历数组等操作。不仅限于数组,线性(分布是连续的)数据结构都可以,比如Java对象的各个字段在堆中就是连续的内存块分布(详见Java对象的内存布局)

空间局部性的应用

  1. 主内存缓存硬盘 page cache :kafka,mysql等

mysql某个索引(索引在b+树叶子节点也是线性分布的)被命中后会一同加载该索引前后多条记录到同一个pageCache中,这样可以减少磁盘IO次数,提升查询性能。kafka的消息log在磁盘中也是按分区内的消息顺序追加的,也可以很好的利用该特性

  1. cpu高速缓存主内存 cache line:redis等

redis中的数据结构散列集Hash,当key数量小于一定值时散列集会被压缩成数组就是为了利用cpu高速缓存,这也是为什么使用Hash比使用多个KV要快的原因。详见redis官网-内存优化

MESI缓存一致性协议(扩展)

缓存一致性协议MESI

  • M(修改,Modified)

本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有)

  • E(专有,Exclusive)

缓存行内容和内存中的一样,而且其它处理器都没有这行数据

  • S(共享,Shared)

缓存行内容和内存中的一样,有可能其它处理器也存在此缓存行的拷贝

  • I(无效,Invalid)

缓存行失效, 不能使用

Jvm堆中对象的内存布局

本文使用 mdnice 排版



发布于: 2020 年 07 月 31 日 阅读数: 6
用户头像

刚刚🏂

关注

还未添加个人签名 2020.03.13 加入

还未添加个人简介

评论

发布
暂无评论
并发编程基础原理