synchronized重量级锁原理3

62 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第32天,点击查看活动详情

继续接上篇文章,被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor 的竞争。

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
	TEVENT (Inflated enter - park TIMED) ;
	Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
	RecheckInterval *= 8 ;
	if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
	TEVENT (Inflated enter - park UNTIMED) ;
	Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ;

monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。 用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

image.png

从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存 储资源、I/O资源等。 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执 行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内 核态)。 系统调用的过程可以简单理解为:

  1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。
  2. 用户态程序执行系统调用。
  3. CPU切换到内核态,并跳到位于内存指定位置的指令。
  4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。 由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在 synchronized未优化之前,效率低的原因。

锁降级的争论

1、先说结论,在openjdk的hotsopt jdk8u里是有锁降级的机制的,锁降级是什么时候加入到hotspot的这个我没去关注,所以我只说看过代码的jdk8u版本,另外根据R大的这个回答,我相信sunj dk也一样。

2、然后再详细说:

  • 锁降级的代码在deflate_idle_monitors方法中,其调用点在进入SafePoint的方法SafepointSynchronize::begin()中。
    deflate_idle_monitors中会找到已经idle的monitor(也就是重量级锁的对象),然后调用deflate_monitor方法将其降级。
  • 因为锁降级是发生在safepoint的,所以如果降级时间过长会导致程序一直处于STW的阶段。在这里有篇文章讨论了优化机制。jdk8中本身也有个MonitorInUseLists的开关,其影响了寻找idle monitor的方式,对该开关的一些讨论看这里
  • 至于为什么《java并发编程的艺术》中说锁不能降级,我猜测可能该书作者看的jdk版本还没有引入降级机制。