【JUC】synchronized 底层原理及优化过程

synchronized 底层原理

概括来说,synchronized 之所以能通过持有对象的锁实现同步是通过 Monitor (管程)实现的。(Monitor 是操作系统级的对象)
我们看一下普通对象的对象头储存了那些内容

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|              Mark Word (32 bits)   |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

64位系统 Mark Word存储的内容如下,这与锁升级的过程息息相关(最后两位状态码表示是否加锁)

|--------------------------------------------------------------------|--------------------|
|                   Mark Word (64 bits)                              |       State        |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01    |       Normal       |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2     | unused:1 | age:4 | biased_lock:1 | 01    |       Biased       |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62                                      | 00    | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62                              | 10    | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                                            | 11    |    Marked for GC   |
|--------------------------------------------------------------------|--------------------|

加锁的工作原理如下
Monitor原理

  • 刚开始 MonitorOwner(所有者)为 null(Monitor 中只能有一个 Owner )
  • 当使用 synchronized 给对象上锁(重量级锁)之后,该对象的对象头里的 MarkWord 中被设置为指向 Monitor 对象的指针(状态为 Heavyweight
  • 当 Thread-2 执行 synchronized(obj) 时,会将 Monitor 的所有者 Owner 设置为 Thread-2
  • Thread-2 上锁的过程中,若其他线程也来执行 synchronized(obj),就会进入阻塞队列 EntryList (其他线程执行到 synchronized部分时会首先判断 obj 是否关联上 Monitor 锁对象,然后判断 Monitor 锁是否有所有者 Owner)
  • Thread-2 执行完同步代码块中的内容后会唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的(不是先进先出)
  • WaitSet 中是之前获得过锁,但条件不满足进入 Waiting状态的线程(也就是执行了 wait 方法的线程)

synchronized 优化

上文提到的使用 Monitor 进行加锁,属于重量级锁,在 JDK 1.6 以前只有重量级锁,效率较低。在JDK 1.6之后,为了减少获得锁和释放锁所带来的性能消耗,从 JVM 层面对 synchronized 进行了优化,引入了偏向锁和轻量级锁。

锁的升级过程:偏向锁 -> 轻量级锁 -> 重量级锁
(整个过程由 JVM 控制)

轻量级锁

使用场景:

如果一个对象虽然有多个线程访问,但是多线程访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized。系统会优先使用轻量级锁方式加锁,轻量级锁加锁失败后,(进入锁膨胀流程)才会自动转为重量级锁。

流程:

  • 创建锁记录(Lock Record)对象。(每个线程的栈帧中都包含一个记录锁结构的结构,锁记录对象比 Monitor 更加简单、轻量化,内部可以存储锁定对象的 Mark Word)
  • 让锁记录中的 Object reference (对象指针)指向锁对象,并尝试用 CAS 替换(交换) Object 的 Mark Word,将 Mark Word 的值存入锁记录
  • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态,表示由该线程给对象加锁(CAS 操作是原子性的,不会被打断。最后两位状态码表示加锁与否和加锁状态,01表示未加锁,00 表示轻量级锁,10表示重量级锁)
  • 线程存在竞争CAS 操作失败除了线程存在竞争(其他线程已经持有了该 Object 的轻量级锁)之外还有可能是由锁重入导致的(锁重入不会转为重量级锁)
  • 如果线程在已经执行上一次 CAS 成功之后,再次执行 synchronized 锁重入,那么会在添加一条 Lock Record 作为重入的计数
  • 如果是由线程存在竞争导致的 CAS 操作失败,会进入锁膨胀流程,由轻量级锁转为重量级锁
  • 解锁
    • 如果有取值为 null 的锁记录,表示重入,此时重置锁记录,表示重入计数减一
    • 当锁记录的值不为 null,此时使用 CAS 将 Mark Word 的值恢复给对象 Object
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程
        • 按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,并唤醒 EntryList 中 BLOCKED 线程

轻量级锁工作流程

锁膨胀

出现的原因:

在线程尝试加轻量级锁时,进行 CAS 操作失败(说明此时存在其他线程为此对象加上了轻量级锁,即存在竞争),进入锁膨胀流程将轻量级锁变为重量级锁

流程:为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁的地址,然后自己进入 Monitor 的 EntryList 变为 BLOCKED 状态

自旋优化

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(即持锁线程退出了同步块,释放了锁),当前线程就可以避免阻塞。

  • 避免阻塞意味着避免了上下文切换,可以极大的节约系统资源
  • 自旋简单来说就是让线程不断执行循环(无意义的循环),循环结束后查看锁是否释放
  • 在自旋重试了一定次数之后,当前线程还不能获取对象锁,当前线程仍然会进入阻塞状态
  • 自旋次数是由 JVM 进行控制的,一般根据之前的自旋情况进行处理
  • 自旋也会占用 CPU 时间,对于单核 CPU 而言自旋就是浪费,多核 CPU 自旋才可以发挥优势
  • Java 7 之后无法控制是否开启自旋

偏向锁

为什么需要偏向锁?

这是因为轻量级锁在没有竞争时,每次重入仍然需要执行 CAS 操作,这无疑是没有必要的。

偏向锁优化流程:

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后检查这个线程 ID 是否属于自己,是的话,表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个锁对象就归该线程所有。

偏向状态

在对象头的 Mark Word 值的倒数第三位 biased_lock 表示是否开启了偏向锁(1 表示开启,0 表示未开启),开启之后,MarkWord 的最后三位为 101

偏向锁的开启是延迟的,不会在程序启动之后立即生效。

(如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟)

注意:这里的开启偏向锁指的是可以使用偏向锁,而非已经通过 synchronized 添加锁对象。

禁用偏向锁:添加 VM 参数 -XX:-UseBiasedLocking

撤销偏向锁
调用对象的 hashcode方法

调用 hashcode 方法之后会撤销偏向锁,原因是调用 hashcode 方法后,需要在对象头中存储 hashcode,而此时偏向锁的 mark word 已经存储了 54 位的线程ID,存不下31位的hashcode 了。故而,只能选择撤销偏向锁。

  • 此时轻量级锁会在锁记录中记录 hashcode
  • 此时重量级锁会在 Monitor 中记录 hashcode
其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁(升级轻量级锁之后,变为不可偏向)

调用 wait/notify

由于 wait/notify 都是重量级锁才有的,所以调用这两个方法会将偏向锁转换为重量级锁

批量重偏向

如果对象被多个线程访问,但是没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID(而不是直接撤销偏向锁,升级为轻量级锁)

当撤销偏向锁的阈值超过 20 次后(第20次开始批量重偏向),jvm 才会在给剩下的这些对象加锁时重新偏向至当前加锁线程

批量撤销

当撤销偏向锁的阈值超过 40 次后,JVM 会觉得根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

执行顺序:先对锁对象执行撤销操作,达到批量重定向的阈值之后,将剩下的对象锁批量重定向。然后,继续执行如果达到批量撤销的阈值在对剩下的锁对象执行批量撤销

注意:撤销次数只记录对偏向锁的撤销次数(也就是说一旦由偏向锁变为轻量级锁,后续不论发生什么变化,都不在对其计数)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值