Java面试进阶:synchronized的实现原理和锁的升级降级

同步和锁都是基于AQS框架

synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。

Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。

 

源码层级分析:

synchronized 是 JVM 内部的 Intrinsic Lock,所以偏斜锁、轻量级锁、重量级锁的代码实现,并不在核心类库部分,而是在 JVM 的代码中。

synchronized 的行为是 JVM runtime 的一部分,所以我们需要先找到 Runtime 相关的功能实现。通过在代码中查询类似“monitor_enter”或“Monitor Enter”,很直观的就可以定位到:

sharedRuntime.cpp/hpp,它是解释器和编译器运行时的基类。synchronizer.cpp/hpp,JVM 同步相关的各种基础逻辑。

sharedRuntime:

1)UseBiasedLocking检查是否开启偏斜锁。

2)fast_enter :完整锁获取路径,其依赖synchronizer.cpp-》其biasedLocking通过 CAS 设置 Mark Word 

3)slow_enter 则是绕过偏斜锁,直接进入轻量级锁获取逻辑。

对象头中 Mark Word 的结构:

 

 除了synchronized 和 ReentrantLock,Java 核心类库中还有其他一些特别的锁类型,具体请参考下面的图。

为什么我们需要读写锁(ReadWriteLock)等其他锁呢?

ReentrantLock 和 synchronized 简单实用,但“太霸道”,要么不占,要么独占。有的时候不需要大量竞争的写操作,而是以并发读取为主,ReadWriteLock写的时候不能读;StampedLock写的时候也可以读

 

自旋锁:自旋是种乐观情况的优化

竞争锁的失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。

适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。

乐观锁 悲观锁是一种思想。

乐观锁:即认为读多写少,遇到并发写的可能性比较低,所以采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。(落地页修改时版本号的判断也是乐观锁),乐观锁没法解决ABA问题。

你可能感兴趣的:(java&JVM,java,多线程,面试)