JVM学习笔记——内存模型篇( 五 )

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费 , 多核 CPU 自旋才能发挥优势 。
  • Java 7 之后不能控制是否开启自旋功能
  • 首先我们给出自旋成功的流程展示:
    线程 1 (cpu 1 上)对象 Mark线程 2 (cpu 2 上)-10(重量锁)-访问同步块,获取 monitor10(重量锁)重量锁指针-成功(加锁)10(重量锁)重量锁指针-执行同步块10(重量锁)重量锁指针-执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor执行同步块10(重量锁)重量锁指针自旋重试执行完毕10(重量锁)重量锁指针自旋重试成功(解锁)01(无锁)自旋重试-10(重量锁)重量锁指针成功(加锁)-10(重量锁)重量锁指针执行同步块-......然后我们给出自旋失败的流程展示:
    线程 1(cpu 1 上)对象 Mark线程 2(cpu 2 上)-10(重量锁)-访问同步块,获取 monitor10(重量锁)重量锁指针-成功(加锁)10(重量锁)重量锁指针-执行同步块10(重量锁)重量锁指针-执行同步块10(重量锁)重量锁指针访问同步块 , 获取 monitor执行同步块10(重量锁)重量锁指针自旋重试执行同步块10(重量锁)重量锁指针自旋重试执行同步块10(重量锁)重量锁指针自旋重试执行同步块10(重量锁)重量锁指针阻塞-......偏向锁我们首先来介绍一下偏向锁:
    • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作,Java 6 中引入了偏向锁来做进一步优化
    • 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头 , 之后发现这个线程 ID是自己的就表示没有竞争,不用重新 CAS.
    我们给出偏向锁的一些补充信息:
    • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
    • 访问对象的 hashCode 也会撤销偏向锁
    • 如果对象虽然被多个线程访问 , 但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2 , 重偏向会重置对象的 Thread ID
    • 撤销偏向和重偏向都是批量进行的 , 以类为单位
    • 如果撤销偏向到达某个阈值 , 整个类的所有对象都会变为不可偏向的
    • 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
    我们采用轻量级锁的代码但是加入了偏向锁之后的流程:
    线程 1对象 Mark访问同步块 A,检查 Mark 中是否有线程 ID101(无锁可偏向)尝试加偏向锁101(无锁可偏向)对象 hashCode成功101(无锁可偏向)线程ID执行同步块 A101(无锁可偏向)线程ID访问同步块 B , 检查 Mark 中是否有线程 ID101(无锁可偏向)线程ID是自己的线程 ID,锁是自己的,无需做更多操作101(无锁可偏向)线程ID执行同步块 B101(无锁可偏向)线程ID执行完毕101(无锁可偏向)对象 hashCode其它优化我们下面来简单介绍一下其他的几种优化:
    1. 减少上锁时间
    /*上锁期间的代码是影响上锁时间的最大因素我们应该确保同步代码块中尽量短*/
    1. 减少锁的粒度
    /*将一个锁拆分为多个锁提高并发度例如:LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高*/
    1. 锁粗化
    /*多次循环进入同步块不如同步块内多次循环另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)例如:new StringBuffer().append("a").append("b").append("c");*/
    1. 锁消除
    /*JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量 , 不会被其它线程所访问到 , 这时候就会被即时编译器忽略掉所有同步操作 。*/
    1. 读写分离
    /*CopyOnWriteArrayListConyOnWriteSet*/结束语到这里我们JVM的内存模型篇就结束了 , 希望能为你带来帮助~
    附录该文章属于学习内容 , 具体参考B站黑马程序员满老师的JVM完整教程
    这里附上视频链接:01-JMM-概述_哔哩哔哩_bilibili

    推荐阅读