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

735 阅读5分钟

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

monitor-enter

获取锁和锁升级过程

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

  • kUnlocked 无锁状态

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

  • kThinLocked 轻量级锁

轻量级锁会判断lockword里面存储的线程id,如果当前线程已经持有锁了: image.png 这里会把轻量级锁的数量加1,如果数量达到 kThinLockMaxCount 这个阈值,就会升级成重量级锁。 如果不是当前线程持有这个锁,那就一直自旋尝试获取: image.png 如果自旋次数 > kExtraSpinIters阈值,那么会执行 sched_yield,放弃争夺cpu。 否则会升级为重量级锁。

  • kFatLocked 重量级

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

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

锁升级过程

锁升级过程主要在InflateThinLocked函数。如果lockword里面维护的线程id是当前线程,会直接升级: image.png 如果不是当前线程,那么会暂停持有锁的线程,将他升级成重量级锁之后再恢复线程:image.png 这个地方说明只要有一个线程持有了重量级锁,那么其他线程也会升级为重量级锁。 Inflate函数里调用Install函数,根据持有锁的的对象的lockword状态判断:

  • 轻量级锁

通过cas把lockword更新成重量级锁 image.png

  • 重量级锁

不用处理 image.png

加锁过程

加锁过程在Monitor的Lock函数里面。Lock的时候会先自旋尝试获取锁: image.png 后面会使用mutex去加锁: image.png

monitor-exit

释放锁过程

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

  • 无锁

image.png 这种是执行失败的情况。

  • 轻量级锁

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

  • 重量级锁

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

解锁过程

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

wait 和 notify

wait

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

  • wait_set_ 等待中的线程队列
  • wake_set_ 竞争中的线程队列

先定义线程状态: image.png 重载的Wait里面会把线程暂停修改为waiting状态:image.png 调用wait的时候所在线程会加入 wait_set_队列。 image.png 接着会执行SignalWaiterAndReleaseMonitorLock函数,这个函数在 wake_set_ 内有线程的时候,监听Signal信号,当监听到Singal的时候,循环结束,走到释放流程。 image.png 接着会发出Wait信号: image.png 这个Wait信号对应的真正操作是 pthread_con_wait 信号: image.png 我们查询一下这个函数的文档: image.png 这是一个基于条件变量的阻塞,可以通过pthread_cond_signal来恢复线程。而SignalWaiterAndReleaseMonitorLock里面的Signal就对应的这个调用。

notify/notifyAll

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

  • notify

image.png

  • notifyAll

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

总结

synchronized的实现原理:

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

锁升级过程总结成这张图: image.png

  • 对象调用wait、notify/notifyAll的时候需要再同步代码块里执行
  • notify/notifyAll只是修改线程队列,阻塞和恢复的逻辑都维护在wait里面。