synchronized底层如何实现?

典型回答

如果你使用反编译工具查看synchronized代码块,会发现是由一对儿monitorentry/monitorexit指令实现的,Monitor对象是同步的基本实现单元。

在Java 6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。

现代的(Oracle)JDK中,JVM对此进行了大刀阔斧地改进,提供了三种不同的Monitor实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

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

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

以上描述了锁的升级过程。而当JVM进入安全点(SafePoint)的时候,会检查是否有闲置的Monitor,然后试图进行降级。

知识扩展

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

1、偏斜锁(Biased Locking)

偏斜锁,顾名思义,它会偏向于第一个访问锁的线程。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏斜锁。如果在运行过程中,遇到了其它线程抢占锁,则持有偏斜锁的线程会被挂起,JVM会消除它身上的偏斜锁,将锁恢复到标准的轻量级锁。

偏斜锁获取过程如下:

1)访问Mark Word中偏斜锁的标识是否设置成1,锁标志为是否为01,确认为可偏斜状态。
2)如果为可偏斜状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3.
3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
4)如果CAS获取偏斜锁失败,则表示有竞争。当到达全局安全点(SafePoint)时获得偏斜锁的线程被挂起,偏斜锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏斜锁的时候会导致stop the word)
5)执行同步代码。

偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的synchronized块时,才能体现出明显改善。实践中对于偏斜锁一直是有争议的,有人甚至认为,当你需要大量使用并发类库时,往往意味着你不需要偏斜锁。从具体选择来看,我还是建议需要在实践中进行测试,根据结果再决定是否使用。

还有一方面,偏斜锁会延缓JIT预热的进程,所以很多性能测试中会显式地关闭偏斜锁,命令如下:

-XX:-UseBiasedLocking

2、轻量级锁

轻量级锁是由偏斜锁升级来的,加锁过程如下:

1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),JVM首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。
2)拷贝对象头中的Mark Word复制到锁记录中。
3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将LockRecord里的owner指针指向Object Mark Word。如果更新成功,则执行步骤4,否则执行步骤5。
4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
5)如果这个更新操作失败了,JVM首先会检查对象的Mark Word是否指向当前线程的堆栈,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志位的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

你可能感兴趣的:(Java编程面试)