最近打算了解下死锁监控,于是先探究了下java在Android art平台synchronized的原理,看了下相关的源码,这里分享一下相关的实现细节。
synchronized在底层是通过moniter监视器来实现的,获取锁的时候,会生成一个monitor-enter指令,释放锁的时候会生成一个monitor-exit指令。monitor的相关实现在 monitor.h和 monitor.cc里面。
monitor-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去加锁:
monitor-exit
释放锁过程
释放锁的时候,会执行Monitor::MonitorExit函数。monitor-exit的时候也会在死循环里面自旋,根据lockword的状态来判断执行不同逻辑。
- 无锁
这种是执行失败的情况。
- 轻量级锁
如果lockword存储的线程id不是当前线程,执行失败。
exit的过程中轻量级锁会更新数量,每次减1。
- 重量级锁
重量级锁直接调用Unlock函数解锁。
解锁过程
这里如果是当前线程,会把lock_count减去1,当lock_count为0的时候,说明可以正式释放锁。调用SignalWaiterAndReleaseMonitorLock函数。这个函数后面再看。
wait 和 notify
wait
Java层调用Object的wait和notify方法的时候,也是通过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代码块里面调用。
总结
synchronized的实现原理:
- 线程获取锁的时候会执行monitor-ente,释放锁的时候会执行monitor-exit
- 对象的结构里通过LockWord维护了锁的状态,锁的状态会决定锁的量级。从无锁状态去获取锁的时候,会把LockWord更新为轻量级锁。轻量级锁用cas方式获取锁,如果超过一定自旋次数没有成功获取轻量级锁,那么锁会升级到重量级锁。
- 重量级锁去竞争锁的时候会阻塞线程等待获取锁,底层会使用mutex lock实现加锁,这个会涉及jvm线程的阻塞和恢复,所以性能消耗是最大的。
锁升级过程总结成这张图:
- 对象调用wait、notify/notifyAll的时候需要再同步代码块里执行
- notify/notifyAll只是修改线程队列,阻塞和恢复的逻辑都维护在wait里面。