第二章 java并发机制的底层实现原理
- java代码执行流程
- java代码编译成class文件(字节码);
- class被类加载器加载到JVM中;
- JVM执行class,生成汇编码;
- 汇编码(转化成机器码/cpu指令)在cpu上执行.
因此java并发机制的底层实现依赖于两个层面:
- class=>汇编码过程中增加的指令;
- 并发相关cpu指令的具体执行过程.
2.1 volatile
定义
可见性. 对于volatile变量,所有线程看到的值是一致的.
换句话说,某个线程对于volatile的修改能立即生效.实现
- class=>指令: 增加Lock指令
- Lock指令具体执行:
(1) 将当前cpu包含该值的缓存行写回内存;
(2) 在总线上通知其他cpu这个地址已发生更改,需要刷新缓存.
(缓存一致性协议)
- 相关优化
由于上述实现中的2(1)为:”将当前cpu包含该值的缓存行写回内存”,
换句话说,如果这个值跨行了,就会影响两行的数据,也就会导致两行的缓存失效,
其他cpu刷新缓存的数据量变成两倍.
因此尽量要把数据对齐到一行. (比如32位,64位)
2.2 synchronized
内置锁,锁某个对象.(可以是当前实例对象或当前类对象)
- 实现
class=>指令:
增加:
进入同步块: monitorenter
离开同步块: monitorexit (正常离开或者异常)cpu对这俩指令的执行书里没有细讲,只说了对象头里相关数据是怎么存的.
2.2.1 对象头中锁相关数据
- 对象头内容
- Mark Word: hashCode/分代年龄/锁信息.
- 类元数据地址;
- 数组长度. // 如果是数组
- 不同锁标志的信息
- 轻量级锁: 指向栈中锁记录的指针; // 锁标志00
- 重量级锁: 指向互斥量的指针 ; // 锁标志10
- GC标记 : 空; // 锁标志11
- 偏向锁 : 线程ID,epoch,分代年龄,1; // 锁标志01
锁的4种状态: (锁只能升级,不能降级)
- 无锁;
- 偏向锁: 一个线程使用该对象;
- 轻量级锁: 多个线程交替使用该对象;
- 重量级锁: 多个线程同时竞争该对象.
偏向锁
HotSpot作者:
大多数情况下不存在多个线程竞争一个对象,这个时候可以优化让线程获得锁的代价更低.
- 获取偏向锁流程
- 检查对象头里线程ID是不是自己或者是否无锁状态;
- 复制对象头中Mark Word到栈中;
- 在副本上写线程ID为自己ID;
- CAS,用副本替换Mark Word,获得偏向锁.
如果成功的话,下次进入同步块的时候,只要第1步能成功,就不再需要CAS操作了.
换句话说,这种场景下, 同一个线程可以重入同一个对象的锁,只有第一次需要CAS操作(代价比较大的操作).
- 偏向锁的撤销(也就是对象头中存储的线程ID改掉)
- 别的线程也申请这个锁;
- 之前拥有锁的线程不存活=> 对象头设置成无锁;
- 之前拥有锁的线程存活 => 锁升级.
轻量级锁
- 获取轻量级锁流程
- 检查
- 复制对象头中Mark word到栈中;
- 副本中写指向自己锁记录的指针;
- CAS,用副本替换Mark word,获得偏向锁.
如果第4步失败,尝试先不阻塞,使用自旋获取锁.(有可能已经拥有这个锁,试试看)
如果又失败,膨胀(升级)为重量级锁.
- 轻量级锁的解锁
- CAS还原复制的对象头.
如果成功,就解锁;
如果失败,说明除了自己还有别人也改过对象头.膨胀为重量级锁.
对比:
- 偏向锁: 打个自己的标记;
- 轻量级锁: 不阻塞,自旋重试;
- 重量级锁: 阻塞,等待唤醒.
2.3 原子操作实现原理
- 总线锁: 某个cpu用Lock指令锁总线,独占内存; // 开销大
- 缓存锁: 某个cpu修改内存地址,使其他cpu缓存无效.//开销小