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

Posted 公众号JavaEdge

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#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干货盘点#

BasicLock 类型的 ​_lock​ 对象主要用来保存 ​_obj​ 对象的对象头数据:

#yyds干货盘点#

1.1.2 函数体

#yyds干货盘点#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​​函数实现

#yyds干货盘点#

膨胀过程的实现比较复杂,截图中只是一小部分逻辑,完整的方法可以查看​​synchronized.cpp​​,大概实现过程如下:

1、整个膨胀过程在自旋下完成;

2、​​mark->has_monitor()​​方法判断当前是否为重量级锁,即Mark Word的锁标识位为 ​10​,如果当前状态为重量级锁,执行步骤(3),否则执行步骤(4);

3、​​mark->monitor()​​方法获取指向ObjectMonitor的指针,并返回,说明膨胀过程已经完成;

4、如果当前锁处于膨胀中,说明该锁正在被其它线程执行膨胀操作,则当前线程就进行自旋等待锁膨胀完成,这里需要注意一点,虽然是自旋操作,但不会一直占用cpu资源,每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起;如果其他线程完成锁的膨胀操作,则退出自旋并返回;

5、如果当前是轻量级锁状态,即锁标识位为 ​00​,膨胀过程如下:

#yyds干货盘点#

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​​方法中。

#yyds干货盘点#

1、通过CAS尝试把monitor的_owner字段设置为当前线程;

2、如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ,记录重入的次数;

3、如果之前的_owner指向的地址在当前线程中,这种描述有点拗口,换一种说法:之前_owner指向的BasicLock在当前线程栈上,说明当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程,该线程成功获得锁并返回;

4、如果获取锁失败,则等待锁的释放;

monitor等待

monitor竞争失败的线程,通过自旋执行​​ObjectMonitor::EnterI​​方法等待锁的释放,EnterI方法的部分逻辑实现如下:

#yyds干货盘点#

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​​方法中。

#yyds干货盘点#

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/





以上是关于#yyds干货盘点# synchronize底层实现原理-重量级锁的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点#Java并发机制的底层实现原理

ConcurrentHashMap底层实现#yyds干货盘点#

#yyds干货盘点#面试官让我聊聊synchronized

#yyds干货盘点# Java | 关于synchronized相关理解

#yyds干货盘点#面试官synchronized连环问,学会Monitor之后轻松拿下

#yyds干货盘点#线程通信