synchronized
的底层是由一对 monitorenter
/monitorexit
指令实现的,Monitor
对象是同步的基本实现单元。
Java 6 之前,Monitor
的实现完全是依靠操作系统内部的互斥锁来实现的,这种机制需要进行用户态到内核态的切换,所以在 Java 6 之前,同步都是无差别的重量级操作。
之后的 jdk 中做了优化,提供了三种不同的 Monitor
实现,分别是:
-
1.偏斜锁 (Biased Locking)
-
2.轻量级锁
-
3.重量级锁
所谓锁的升级,降级,实际上是 JVM 对 synchronized
优化的一种策略,JVM 会检测不同的竞争状态,然后自动切换到合适的锁实现,这种切换就是锁的升级,降级。
当没有出现锁的竞争时,默认使用的是偏斜锁。JVM 会利用 CAS 操作,在对象的头上的 Mark Word 部分设置线程 ID, 用来表示当前对象偏向于当前线程,所以并不涉及真正的互斥锁。这种策略是基于现实很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏斜锁可以降低无竞争锁的开销。
如果有另一个线程试图锁定某个以被加持偏斜锁的对象时,JVM 就需要撤销偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark World 来试图获取锁,如果获取成功,就使用轻量级锁,否者,进一步升级到重量级锁。
题外话 1:网上有很多观点这样说,Java 不会进行锁的降级。
这种观点实际上错误的,锁的降级确实会发生。当 JVM 进入安全点时,会检查是否有闲置的 Monitor
, 让后试图进行锁的降级。
补充知识点:
自旋锁
何为自旋锁?竞争锁失败的线程,并不会真实的在操作系统层面被挂起,JVM 会让线程做几个空循环(基于预测在短时间内能够获取到锁),经过若干次循环后,如果可以获取到锁,那么会进入到临界区,如果还不能获取到锁,才会真实在操作系统层面被挂起。
适用场景:
自旋锁可以减少线程的阻塞,这对于锁竞争不是很激烈,且占用锁时间非常短的代码来说,将会带来很大的性能提升,因为自旋的消耗会小于线程被挂起带来的消耗。
但是如果锁竞争激烈,或者持有锁的线程运行时间很长,就不适用于使用自旋锁了,应为自旋锁在获取到锁前一直都是占用 CPU 做无用功,线程自旋的消耗就大于线程被挂起带来的消耗了,造成 CPU 的浪费。
总结:自旋锁实际上是对乐观情况的优化。