查看原文
其他

Android线程锁机制:monitor机制解析

鸿洋
2024-08-24

The following article is from 半行代码 Author 半行代码

最近打算了解下死锁监控,于是先探究了下java在Android art平台synchronized的原理,看了下相关的源码,这里分享一下相关的实现细节。synchronized在底层是通过moniter监视器来实现的,获取锁的时候,会生成一个monitor-enter指令,释放锁的时候会生成一个monitor-exit指令。monitor的相关实现在 monitor.hmonitor.cc里面。

1monitor-enter

获取锁和锁升级过程
monitor-enter Monitor::MonitorEnter函数.monitorEnter 里面会判断当前的锁状态,来决定是否需要锁升级。锁的状态维护在Object里的LockWord对象里面,每次monitorEnter的时候都会在死循环里面自旋判断LockWord的状态进行下一步操作:
  • kUnlocked 无锁状态

当前是无锁状态,这里会通过cas升级到kThinLocked。

  • kThinLocked  轻量级锁
轻量级锁会判断lockword里面存储的线程id,如果当前线程已经持有锁了:

这里会把轻量级锁的数量加1,如果数量达到 kThinLockMaxCount 这个阈值,就会升级成重量级锁。如果不是当前线程持有这个锁,那就一直自旋尝试获取:

如果自旋次数 > kExtraSpinIters阈值,那么会执行 sched_yield,放弃争夺cpu。否则会升级为重量级锁。

  • kFatLocked 重量级

这里直接调用Monitor的Lock函数。

实际的源码里可以看出来,在ART虚拟机的实现里面,是没有偏向锁的实现的。和Java的HotSpot实现并不一样。猜测原因是偏向锁的实现不算简单,并且存在竞争的时候很容易就升级成轻量级锁,所以在ART没有实现,省的浪费性能。并且我查了下,Java也是可以通过命令行参数关闭偏向锁,并且最新的JDK里也是废弃了偏向锁的。
锁升级过程
锁升级过程主要在InflateThinLocked函数。如果lockword里面维护的线程id是当前线程,会直接升级:

如果不是当前线程,那么会暂停持有锁的线程,将他升级成重量级锁之后再恢复线程:

这个地方说明只要有一个线程持有了重量级锁,那么其他线程也会升级为重量级锁。Inflate函数里调用Install函数,根据持有锁的的对象的lockword状态判断:

  • 轻量级锁
通过cas把lockword更新成重量级锁。

  • 重量级锁

不用处理。

加锁过程
加锁过程在Monitor的Lock函数里面。Lock的时候会先自旋尝试获取锁:

后面会使用mutex去加锁:


2monitor-exit

释放锁过程

释放锁的时候,会执行Monitor::MonitorExit函数。monitor-exit的时候也会在死循环里面自旋,根据lockword的状态来判断执行不同逻辑。
  • 无锁

这种是执行失败的情况。

  • 轻量级锁

如果lockword存储的线程id不是当前线程,执行失败。exit的过程中轻量级锁会更新数量,每次减1。

  • 重量级锁

重量级锁直接调用Unlock函数解锁。

解锁过程

这里如果是当前线程,会把lock_count减去1,当lock_count为0的时候,说明可以正式释放锁。调用SignalWaiterAndReleaseMonitorLock函数。这个函数后面再看。

3wait 和 notify

wait
Java层调用Object的waitnotify方法的时候,也是通过monitor在实现的。Wait函数有2个,先看第一个,调用了Wait之后,lockword里的状态会标记为重量级锁:

monitor里面定义了2个队列:

  • wait_set_   等待中的线程队列。
  • wake_set_ 竞争中的线程队列。
先定义线程状态:

重载的Wait里面会把线程暂停修改为waiting状态:

调用wait的时候所在线程会加入 wait_set_队列。

接着会执行SignalWaiterAndReleaseMonitorLock函数,这个函数在 wake_set_ 内有线程的时候,监听Signal信号,当监听到Singal的时候,循环结束,走到释放流程。

接着会发出Wait信号:

这个Wait信号对应的真正操作是 pthread_con_wait 信号:

我们查询一下这个函数的文档:

这是一个基于条件变量的阻塞,可以通过pthread_cond_signal来恢复线程。而SignalWaiterAndReleaseMonitorLock里面的Signal就对应的这个调用。

notify/notifyAll
我们接着看下notify和notifyAll调用。
  • notify
  • notifyAll

notify和notifyAll调用是类似的,就是把 wait_set_里面的内容移动到 wake_set_里面。这样就对应上了SignalWaiterAndReleaseMonitorLock里面的循环。所以notify和notifyAll只是修改一下队列,阻塞和恢复逻辑都是在wait里面实现的。在wait、notify/notifyAll的调用里面有一个细节,当前线程不持有锁的时候,会抛出“object not locked by thread before wait()”异常:

这里也对应了我们使用对象wait、notify的时候,我们需要在synchronized代码块里面调用。

4总结


synchronized的实现原理:
  • 线程获取锁的时候会执行monitor-ente,释放锁的时候会执行monitor-exit
  • 对象的结构里通过LockWord维护了锁的状态,锁的状态会决定锁的量级。从无锁状态去获取锁的时候,会把LockWord更新为轻量级锁。轻量级锁用cas方式获取锁,如果超过一定自旋次数没有成功获取轻量级锁,那么锁会升级到重量级锁。
  • 重量级锁去竞争锁的时候会阻塞线程等待获取锁,底层会使用mutex lock实现加锁,这个会涉及jvm线程的阻塞和恢复,所以性能消耗是最大的。
锁升级过程总结成这张图:

  • 对象调用wait、notify/notifyAll的时候需要再同步代码块里执行

  • notify/notifyAll只是修改线程队列,阻塞和恢复的逻辑都维护在wait里面。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

ArkTS 入门指南:快速上手鸿蒙应用开发
Android特效视频Surface+Camera2的实现
包体积优化:Android编译期PNG自动化转换WEBP


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存