2

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

 2 years ago
source link: https://blog.51cto.com/u_11440114/5080451
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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底层实现原理(3)-重量级锁_内核态

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

#yyds干货盘点# synchronize底层实现原理(3)-重量级锁_字段_02

1.1.2 函数体

#yyds干货盘点# synchronize底层实现原理(3)-重量级锁_字段_03UseBiasedLocking​ 标识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干货盘点# synchronize底层实现原理(3)-重量级锁_用户态_04

膨胀过程的实现比较复杂,截图中只是一小部分逻辑,完整的方法可以查看​​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干货盘点# synchronize底层实现原理(3)-重量级锁_内核态_05

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干货盘点# synchronize底层实现原理(3)-重量级锁_用户态_06

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

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

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

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

monitor等待

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

#yyds干货盘点# synchronize底层实现原理(3)-重量级锁_字段_07

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干货盘点# synchronize底层实现原理(3)-重量级锁_内核态_08

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 we've set _owner = NULL we can't 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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK