synchronized实现原理与锁的优化

原文地址

1. synchronized 实现原理

1.1 获取锁

  • synchronize 修饰语句块

    通过获取对象内部的监视器锁实现,使用了 monitorenter 和 monitorexit 指令,这两条指令在编译字节码时分别插入同步代码块的开始和结束位置(查看字节码可以使用 JDK 的 javap 命令,也可以使用 idea 的插件 jclasslib)

    image-20210323195626666

  • synchronized 修饰方法

    flags 使用 ACC_SYNCHRONIZED 访问标志,表明该方法是一个同步方法

    javap -private -verbose Main.class

    image-20210323195417138

1.2 线程状态和和状态转化

  • 在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现,里面有几个重要属性

    • owner:持有 monitor 的线程
    • waitSet:处于 wait 状态 的线程
    • entrySet:处于 block 状态的线程
  • 转换过程

    • 当多个线程同时访问一段同步代码时,首先进入 entrySet
    • 当某个线程获取到对象的 monitor 后,进入 The Owner 区域,并将 owner 设置为当前线程,同时 count +1
    • 如果线程调用 wait 方法,释放对象的 monitor,进入 Wait Set 区域,并将 owner 设置为 null,同时 count -1
    • 如果线程执行完毕,释放对象的 monitor 并复位变量的值,以便其他线程进入获取 monitor
    image-20210323200855290

2. 锁优化

2.1 减少锁竞争的优化技术

轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除

锁只能从低级升级到高级:无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁

2.2 重量级锁

monitor 监视器锁本质是依赖 OS 的 Mutex Lock 互斥量来实现的,因为 OS 实现线程间的切换需要从用户态转换到核心态,这个过程成本较高、耗时较长,因此称为重量级锁。

2.3 对象头

在了解轻量级锁和偏向锁之前,先从对象头(Object Hander)开始,对象头分为两部分:

  • Mark Word:存储对象自身的运行时数据,如 hashcode、GC 分代年龄、锁信息,Mark Word 在 32 位的 JVM 中是 32 bit

    image-20210323171838983

  • 存储指向方法区对象类型数据的指针,如果是数组对象的话,额外存储数据的长度

2.4 轻量级锁

  • 轻量级锁是相对基于 OS 的互斥量(Mutex Lock)而言的,在没有多线程竞争的情况下,避免使用 OS 的互斥量带来的性能消耗。

  • 轻量级锁认为,对于绝大部分锁,在整个同步周期内都是不存在竞争的,如果没有竞争,轻量级锁就可以使用 CAS 操作避免互斥量的开销,从而提高效率。

  • 获取轻量级锁的过程

    1. 线程执行同步块,JVM 会现在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前的 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如下图所示
    image-20210323202120363
    1. JVM 使用 CAS 操作尝试将对象头的 Mark Word 更新为指向 Lock Record 的指针

    2. 如果更新成功,那么该线程就获得对象的锁,对象的 Mark Word 的锁状态为轻量级锁。此时堆栈与对象头的状态如下图所示

      image-20210323202706707
    3. 更新失败,JVM 首先检查对象的 Mark Word 是否指向当前线程的栈帧

      • 如果是,说明当前线程已经获得该对象的锁,可以直接进入同步代码块继续执行
      • 如果不是,说明锁对象已被其他线程抢占,当前线程会尝试自旋一定次数来获取锁
        • 如果自旋一定次数 CAS 操作后仍然没有成功,那么轻量级所就要升级为重量级锁,Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也就进入阻塞状态
  • 释放轻量级锁的过程

    1. 通过 CAS 操作用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word
    2. 如果替换成功,整个同步过程就完成了
    3. 如果替换失败,说明有其他线程尝试获取过改该锁,那就在释放锁的同事,唤醒被挂起的线程

2.5 偏向锁

  • 轻量级锁是在无多线程竞争的情况下,使用 CAS 操作去消除互斥量,而偏向锁是在无多线程的情况下,将这个同步动作都消除掉。

  • 偏向锁认为,对于绝大部分锁,在整个同步周期内不仅不存在锁竞争,而且总是由同一线程获取,持有偏向锁的线程不需要再进行同步,从而提高效率。

  • 获取偏向锁的过程

    1. 线程执行同步块,首次获取锁对象时,JVM 会将锁对象的 Mark Word 的锁状态设置为偏向锁,同时通过 CAS 将获取到锁的 ThreadID 设置到 Mark Word 中

    2. 如果 CAS 操作成功,持有偏向锁的线程每次进入和退出同步代码块时,只需判断 Mark Word 里是否存储当前 ThreadID,如果是,则表示线程已经获得锁,不需要额外进行 CAS 操作加锁和解锁

    3. 如果不是,则需要通过 CAS 操作竞争锁,当竞争成功时,将 Mark Word 的 ThreadID 替换为当前线程 的 ThreadID

  • 释放偏向锁的过程

    1. 当一个线程已经持有偏向锁,而另一个线程尝试竞争偏向锁时, CAS 替换 ThreadID 操作失败,则开始撤销偏向锁
    2. 如果原持有偏向锁的线程不处于活动状态或已经退出同步代码块,则该线程释放锁,将对象头设置为无锁状态
    3. 如果原持有偏向锁的线程为退出同步代码块,则升级为轻量级锁

2.6 总结

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,只执行非同步方法比只存在纳秒级的差距如果线程之间存在竞争,会带来额外的撤销消耗只有一个线程访问同步代码块的场景
轻量级锁竞争的线程不会阻塞,提升程序响应速度如果始终得不到锁,竞争的线程会自旋消耗 CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间慢追求吞吐量,同步块执行速度较长
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火车站卖橘子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值