java并发编程的艺术笔记-第二章

第二章 java并发机制的底层实现原理

  • java代码执行流程
  1. java代码编译成class文件(字节码);
  2. class被类加载器加载到JVM中;
  3. JVM执行class,生成汇编码;
  4. 汇编码(转化成机器码/cpu指令)在cpu上执行.

因此java并发机制的底层实现依赖于两个层面:

  1. class=>汇编码过程中增加的指令;
  2. 并发相关cpu指令的具体执行过程.

2.1 volatile

  • 定义
    可见性. 对于volatile变量,所有线程看到的值是一致的.
    换句话说,某个线程对于volatile的修改能立即生效.

  • 实现

  1. class=>指令: 增加Lock指令
  2. Lock指令具体执行:
    (1) 将当前cpu包含该值的缓存行写回内存;
    (2) 在总线上通知其他cpu这个地址已发生更改,需要刷新缓存.
    (缓存一致性协议)
  • 相关优化
    由于上述实现中的2(1)为:”将当前cpu包含该值的缓存行写回内存”,
    换句话说,如果这个值跨行了,就会影响两行的数据,也就会导致两行的缓存失效,
    其他cpu刷新缓存的数据量变成两倍.
    因此尽量要把数据对齐到一行. (比如32位,64位)

2.2 synchronized

内置锁,锁某个对象.(可以是当前实例对象或当前类对象)

  • 实现
  1. class=>指令:
    增加:
    进入同步块: monitorenter
    离开同步块: monitorexit (正常离开或者异常)

  2. cpu对这俩指令的执行书里没有细讲,只说了对象头里相关数据是怎么存的.

2.2.1 对象头中锁相关数据

  • 对象头内容
  1. Mark Word: hashCode/分代年龄/锁信息.
  2. 类元数据地址;
  3. 数组长度. // 如果是数组
  • 不同锁标志的信息
  1. 轻量级锁: 指向栈中锁记录的指针; // 锁标志00
  2. 重量级锁: 指向互斥量的指针 ; // 锁标志10
  3. GC标记 : 空; // 锁标志11
  4. 偏向锁 : 线程ID,epoch,分代年龄,1; // 锁标志01

锁的4种状态: (锁只能升级,不能降级)

  1. 无锁;
  2. 偏向锁: 一个线程使用该对象;
  3. 轻量级锁: 多个线程交替使用该对象;
  4. 重量级锁: 多个线程同时竞争该对象.

偏向锁
HotSpot作者:
大多数情况下不存在多个线程竞争一个对象,这个时候可以优化让线程获得锁的代价更低.

  • 获取偏向锁流程
  1. 检查对象头里线程ID是不是自己或者是否无锁状态;
  2. 复制对象头中Mark Word到栈中;
  3. 在副本上写线程ID为自己ID;
  4. CAS,用副本替换Mark Word,获得偏向锁.

如果成功的话,下次进入同步块的时候,只要第1步能成功,就不再需要CAS操作了.
换句话说,这种场景下, 同一个线程可以重入同一个对象的锁,只有第一次需要CAS操作(代价比较大的操作).

  • 偏向锁的撤销(也就是对象头中存储的线程ID改掉)
  1. 别的线程也申请这个锁;
  2. 之前拥有锁的线程不存活=> 对象头设置成无锁;
  3. 之前拥有锁的线程存活 => 锁升级.

轻量级锁

  • 获取轻量级锁流程
  1. 检查
  2. 复制对象头中Mark word到栈中;
  3. 副本中写指向自己锁记录的指针;
  4. CAS,用副本替换Mark word,获得偏向锁.

如果第4步失败,尝试先不阻塞,使用自旋获取锁.(有可能已经拥有这个锁,试试看)
如果又失败,膨胀(升级)为重量级锁.

  • 轻量级锁的解锁
  1. CAS还原复制的对象头.

如果成功,就解锁;
如果失败,说明除了自己还有别人也改过对象头.膨胀为重量级锁.

对比:

  1. 偏向锁: 打个自己的标记;
  2. 轻量级锁: 不阻塞,自旋重试;
  3. 重量级锁: 阻塞,等待唤醒.

2.3 原子操作实现原理

  1. 总线锁: 某个cpu用Lock指令锁总线,独占内存; // 开销大
  2. 缓存锁: 某个cpu修改内存地址,使其他cpu缓存无效.//开销小

推荐文章