《Java 并发编程艺术》
锁优化和逃逸分析:https://juejin.im/post/5dc8f93a6fb9a04a9f11c68a
偏向锁详细:https://zhuanlan.zhihu.com/p/26475023
JDK 1.6 后
使用的锁对象
- 普通同步方法:锁是当前实例对象
- 静态同步方法:锁是当前类的 Class 对象
- 同步方法块:锁是 synchronized 括号里面配置的对象
代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另一种方式实现的。
Java 对象头
synchronized 用的锁是存在 Java 对象头里的。
如果对象是非数组类型,则虚拟机用 2 个字宽(Word)存储对象头。
如果对象是数组类型,则使用 3 个字宽。
32 位虚拟机中,1 字宽等于 4 字节,即 32bit。
对象状态:
锁只能升级但不能降级
偏向锁:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需要简单地测试以下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
如果测试成功,标识线程已经获得了锁。
如果测试失败,则需要再测试以下 Mark Word 中偏向锁的标识是否设置成 1
- 如果没有设置,则说明当前是无锁状态,则使用 CAS 竞争锁
- 如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程
注意:如果一个线程使用过偏向锁后生命周期结束了,此时另外一个线程去竞争偏向锁,是不会导致锁升级的。(可能)
偏向锁的撤销:
偏向锁使用了一种等到竞争出现才释放锁的机制。比如一个线程获得了一个对象的偏向锁,即使这个线程死亡了,这个对象的偏向锁还是存在的,而不会自动地降级回无锁状态。所以当其他线程擦汗给你是竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的策划小,需要等到全局安全点(在这个时间点上没有正在执行的字节码)。
它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着:
- 如果线程不处于活动状态或已退出同步代码块,则将对象头设置为无锁状态
- 如果线程仍然活着,拥有偏向锁的线程会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重写偏向于其他线程,要么恢复到无锁或者标记对象不合适作为偏向锁
最后唤醒暂停的线程。
线程 1 演示了偏向锁初始化的流程,线程 2 演示了偏向锁撤销的流程:
只要线程 1 先获得偏向锁,那么线程 2 CAS 就一定会失败,如果在撤销偏向锁时,线程 1 不处于活动状态或已退出同步代码块,那么线程 2 就可以获得偏向锁,否则锁就会膨胀。
轻量级锁:
加锁:
线程在执行同步块之前,JVM 会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。
然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁就得指针。
- 如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程便擦汗给你是使用自旋来获取锁
解锁:
轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头
- 如果成功,表示没有竞争发生
- 如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
栈帧中的锁记录:
使用 CAS 更新 Mark Word 中 Lock Record 的指针
轻量级锁膨胀流程:
自旋会消耗 CPU,为了避免无用的自旋(比如获得锁的线程在同步块中被阻塞住了),一旦锁升级成重量级锁,就不会恢复到轻量级锁状态。
当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后,会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
整个流程:
来源:Blog.dreamtobe.cn
锁消除:
通过逃逸分析,判断代码是否有必要加锁,从而从编译时期将锁消除。
逃逸分析是怎么判断的?
TODO