#yyds干货盘点# synchronize底层实现原理-重量级锁

宝剑锋从磨砺出,梅花香自苦寒来。这篇文章主要讲述#yyds干货盘点# synchronize底层实现原理-重量级锁相关的知识,希望能为你提供帮助。


1 字节码层实现javap 生成的字节码中包含如下指令:

  • monitorenter
  • monitorexit
synchronized基于此实现了简单直接的锁的获取和释放。
当JVM的解释器执行?monitorenter?时,会进入??InterpreterRuntime.cpp??
1.1 InterpreterRuntime::monitorenter
JRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))

if (UseBiasedLocking)
// 偏向锁,直接进入fast_enter,以避免不必要的锁膨胀
ObjectSynchronizer::fast_enter(h_obj, elem-> lock(), true, CHECK);
else
// 没有开启偏向锁,直接进行轻量级锁加锁
ObjectSynchronizer::slow_enter(h_obj, elem-> lock(), CHECK);

1.1.1 函数参数
  • JavaThread *thread
封装 Java线程 帧状态的与机器/操作系统相关的部分的对象,这里传参代表程序中的当前线程
  • BasicObjectLock *elem
【#yyds干货盘点# synchronize底层实现原理-重量级锁】
BasicLock 类型的 ?_lock? 对象主要用来保存 ?_obj?对象的对象头数据:

1.1.2 函数体
?UseBiasedLocking? 标识JVM是否开启偏向锁功能
  • 如果开启则执行fast_enter逻辑
  • 否则执行slow_enter
2.3 偏向锁
2.4 轻量级锁
轻量级锁自旋抢锁失败后,就会膨胀为重量级锁,并且挂起进入阻塞状态后进入到等待队列等待线程的唤醒,这里阻塞、唤醒就涉及到了用户态和内核态的切换,降低系统性能.
??自旋 : 如果此时持有锁的线程在很短的时间内释放了锁,此时刚进入等待队列的线程又要被唤醒申请资源,这无疑是消耗性能的,而且大多数情况下线程持有锁的时间都不会太长,线程被挂起阻塞可能会得不偿失,所以JVM 提供了一种自旋,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞
??自旋会消耗CPU,所以自旋并不是永久的自旋,而需要控制次数.
// 可设置 JVM 参数来关闭自旋锁,优化系统性能
-XX:-UseSpinning // 参数关闭自旋锁优化 (默认打开)
-XX:PreBlockSpin // 参数修改默认的自旋次数。JDK1.7 后,去掉此参数,由 jvm 控制

2.5 重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
锁膨胀过程锁的膨胀过程通过??ObjectSynchronizer::inflate??函数实现

膨胀过程的实现比较复杂,截图中只是一小部分逻辑,完整的方法可以查看??synchronized.cpp??,大概实现过程如下:
1、整个膨胀过程在自旋下完成;
2、??mark-> has_monitor()??方法判断当前是否为重量级锁,即Mark Word的锁标识位为 ?10?,如果当前状态为重量级锁,执行步骤(3),否则执行步骤(4);
3、??mark-> monitor()??方法获取指向ObjectMonitor的指针,并返回,说明膨胀过程已经完成;
4、如果当前锁处于膨胀中,说明该锁正在被其它线程执行膨胀操作,则当前线程就进行自旋等待锁膨胀完成,这里需要注意一点,虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起;如果其他线程完成锁的膨胀操作,则退出自旋并返回;
5、如果当前是轻量级锁状态,即锁标识位为 ?00?,膨胀过程如下:

1、通过omAlloc方法,获取一个可用的ObjectMonitor monitor,并重置monitor数据;
2、通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,标识当前锁正在膨胀中,如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成;
3、如果CAS成功,设置monitor的各个字段:_header、_owner和_object等,并返回;
monitor竞争当锁膨胀完成并返回对应的monitor时,并不表示该线程竞争到了锁,真正的锁竞争发生在??ObjectMonitor::enter??方法中。

1、通过CAS尝试把monitor的_owner字段设置为当前线程;
2、如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ,记录重入的次数;
3、如果之前的_owner指向的地址在当前线程中,这种描述有点拗口,换一种说法:之前_owner指向的BasicLock在当前线程栈上,说明当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程,该线程成功获得锁并返回;
4、如果获取锁失败,则等待锁的释放;
monitor等待monitor竞争失败的线程,通过自旋执行??ObjectMonitor::EnterI??方法等待锁的释放,EnterI方法的部分逻辑实现如下:

1、当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
2、在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中;
3、node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒,实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z84eXtzV-1571562703110)(https://uploadfiles.nowcoder.com/files/20191020/5088755_1571562670865_4685968-e797fdcdc32a2f8e.png)]
4、当该线程被唤醒时,会从挂起的点继续执行,通过??ObjectMonitor::TryLock??尝试获取锁,TryLock方法实现如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJC8vMmz-1571562703111)(https://uploadfiles.nowcoder.com/files/20191020/5088755_1571562670568_4685968-17d10b24c3369844.png)]
其本质就是通过CAS设置monitor的_owner字段为当前线程,如果CAS成功,则表示该线程获取了锁,跳出自旋操作,执行同步代码,否则继续被挂起;
monitor释放当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于??ObjectMonitor::exit??方法中。

1、如果是重量级锁的释放,monitor中的_owner指向当前线程,即THREAD == _owner;
2、根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过??ObjectMonitor::ExitEpilog??方法唤醒该节点封装的线程,唤醒操作最终由unpark完成,实现如下:
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * Wakee)
assert(_owner == Self, "invariant");

// Exit protocol:
// 1. ST _succ = wakee
// 2. membar #loadstore|#storestore;
// 2. ST _owner = NULL
// 3. unpark(wakee)

_succ = Wakee-> _thread;
ParkEvent * Trigger = Wakee-> _event;

// Hygiene -- once weve set _owner = NULL we cant safely dereference Wakee again.
// The thread associated with Wakee may have grabbed the lock and "Wakee" may be
// out-of-scope (non-extant).
Wakee= NULL;

// Drop the lock
OrderAccess::release_store(& _owner, (void*)NULL);
OrderAccess::fence(); // ST _owner vs LD in unpark()

DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
Trigger-> unpark();

// Maintain stats and report events to JVMTI
OM_PERFDATA_OP(Parks, inc());

3、被唤醒的线程,继续执行monitor的竞争;
总结
  • 三者都需要与线程栈的Lock Record关联,尤其是轻量级锁使用到了Diplaced Mark Word,偏向锁和重量级锁只用到了Object Reference字段.
  • 偏向锁和轻量级锁的加锁解锁是围绕Mark Word 和 Lock Record的关联关系,而重量级锁围绕的是自己向JVM申请的ObjectMonitor对象(重量级锁的情况下,Mark Word存储着指向ObjectMonitor对象的指针)
  • 偏向锁和轻量级锁依靠Lock Record个数来记录重入的次数,而重量级锁通过
ObjectMonitor的整型变量来记录
适用场景
  • 偏向锁 : 偏向锁适合在只有一个线程访问锁的场景,在此种场景下,线程只需要执行一次CAS获取偏向锁,后续该线程可重入访问该锁时仅仅只需要简单的判断Mark Word的线程ID即可
  • 轻量级锁 : 轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争,此种场景下,线程每次获取锁只需要执行一次CAS即可
  • 重量级锁 : 重量级锁适合在多线程竞争环境下访问锁,执行临界区的时间比较长,由于竞争激烈,自旋后未获取到锁的线程将会被挂起进入等待队列,等待持有锁的线程释放锁后唤醒它.此种场景下,线程每次都需要进行多次CAS操作,操作失败将会被放入队列里等待唤醒。
进入重量级锁状态后,线程的阻塞、唤醒操作将严重涉及到操作系统用户态与内核态的切换问题,将严重影响系统性能,所以Java JDB 1.6 引入了 "偏向锁" 和 "轻量锁" 来尽量避免线程用户态与内核态的频繁切换.
应该尽量使 Synchronized 同步锁处于轻量级锁或偏向锁,这样才能提高 Synchronized 同步锁的性能,而通过减小锁粒度来降低锁竞争也是一种最常用的优化方法。
参考
- http://www.itabin.com/synchronized-lock/








    推荐阅读