Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)

文章目录

  • 重量级锁(Mutex Lock)
  • 偏向锁(比较 ThreadID)
    • 偏向锁获取过程
    • 偏向锁的释放
  • 轻量级锁(自旋)
    • 轻量级锁的加锁过程
    • 轻量级锁的释放
  • 总结

重量级锁(Mutex Lock)

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了自旋锁、自适应自旋锁、轻量级锁和偏向锁。

明确 Java 线程切换的代价,是理解java中各种锁的优缺点的基础之一。



Java 对象头中 markword 结构。Java 对象的内存布局及访问方式
Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)_第1张图片

偏向锁(比较 ThreadID)

Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

偏向锁获取过程

  1. 访问 Mark Word 中锁标志位是否为 01,是的话查看偏向锁的标识,如果是 1,则确认为可偏向状态;如果是 0 则为无锁状态,直接通过 CAS 操作竞争锁,如果竞争失败,执行4。

  2. 如果为可偏向状态,则测试线程 ID 是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

  3. 如果线程 ID 并未指向当前线程,则通过 CAS 操作竞争锁。如果竞争成功,则将 Mark Word 中线程 ID 设置为当前线程 ID,然后执行5;如果竞争失败,执行4。

  4. 如果 CAS 获取偏向锁失败,则表示有竞争,开始锁撤销。

  5. 执行同步代码。

偏向锁的释放

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

  1. 当获得偏向锁的线程到达全局安全点(safepoint)时暂停该线程,检查该线程的状态。

  2. 如果该线程存活且没有退出同步代码块,则升级为轻量级锁,并唤醒该线程从安全点继续执行。

  3. 如果该线程没有存活或者该线程已退出同步代码块,则将偏向锁撤销为无锁状态(锁标志位 01,偏向锁标识 0)唤醒该线程。

到达安全点 safepoint 会导致 stop the word,时间很短。

Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)_第2张图片

轻量级锁(自旋)

轻量级是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级锁的加锁过程

1、访问对象的 Mark Word 中锁标识位,如果为 00,JVM 会先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示。(注:该图仅作为参考,我认为该图可能存在问题)
Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)_第3张图片
2、将对象头中的 Mark Word 复制到锁记录中;

3、拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向 object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,此时 Mark Word 的锁标识为 00,这时候线程堆栈与对象头的状态如图所示。(注:该图仅作为参考,我认为该图可能存在问题)
Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)_第4张图片
5、如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则当前线程便尝试使用自旋来获取锁(自旋就是为了不让线程阻塞,而采用循环去获取锁的过程),自旋达到一定次数后 CAS 操作依然没有成功,轻量级锁就要膨胀为重量级锁,锁标识设置为 10,Mark Word 中存储的就是指向重量级锁(monitor)的指针,当前线程以及后面等待锁的线程便会进入阻塞状态。

轻量级锁的释放

轻量级锁解锁时,持有锁的线程会使用 CAS 原子操作将 Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,释放锁并唤醒那些被挂起的线程。

总结

偏向锁
  偏向锁只会在第一次请求锁时使用 CAS 操作,并在锁对象的标记字段中记录当前线程 ID。在此后的运行过程中,仅需比较线程 ID,消除这个线程锁重入(CAS)的开销。针对的是锁仅会被同一线程持有的状况。

轻量级锁
  轻量级锁采用 CAS 操作,减少了传统的重量级锁使用产生的性能消耗。针对的是多个线程在不同时间段申请同一把锁的情况。

重量级锁
  重量级锁会阻塞、唤醒请求加锁的线程,会导致线程上下文切换。针对的是多个线程同时竞争同一把锁的情况。JVM 采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。


参考文章:

  • https://blog.csdn.net/zhao_miao/article/details/84500771
  • https://blog.csdn.net/zqz_zqz/article/details/70233767

你可能感兴趣的:(多线程)