一、前言
编程过程中经常会遇到线程的同步问题,Java 中对同步问题的解决方案比较多(synchronized、JUC、原子操作、volatile、条件变量等),其中synchronized 最方便、简单易用,也是java 编程中使用最多的临界区保护方案。本文主要讲述对象锁的相关知识,详细介绍synchronized 和Object 的关键方法的虚拟机实现原理。
二、Java 对象锁的使用方式
2.1 实例方法的同步
synchronized 修饰实例方法,该同步仅对当前对象的该方法起作用,同一时间只能有一个线程可以进入该对象的此方法。对于不同对象的此函数,无法做到互斥保护。
2.2 静态方法的同步
synchronized 修饰静态方法,该同步对当前类对象的该方法起作用,同一时间只能有一个线程可以进入该方法。
2.3 代码块的同步
在大多少情况下,并不需要对整个方法进行保护,当synchronized 修饰代码块时,该代码块的访问依赖于object 对象锁的互斥访问,同一时间只能有一个线程持有object 对象锁。
更准确的来讲,synchronized 关键字是依赖于对象锁而生效的,每个synchronized 同步块开始的地方都会生成monitor-enter obj指令,同步块结束的地方生成monitor-exit obj 的指令,其中obj 为用于控制互斥访问的对象。同一时间只能有一个线程持有obj 的对象锁。在2.1 中synchronized 依赖的是实列对象,2.2 中synchronized 依赖的是类对象,2.3 中synchronized 依赖的是object 对象。
当一个对象控制多个代码块时,多个代码块也是互斥访问,如下面代码:
代码块①和代码块② 虽然在两个函数中,但是synchronized 依赖的对象都为object,这两个代码块也是互斥访问。
2.4 Object wait() 和notify() 使用方法
Object 作为所有类的基类,都实现了object方法。典型的用法如下:
thread 1持有object 对象锁,并调用object.wait() 方法后,则该线程进入WAITING状态,并释放object 对象锁,等待其它线程来唤醒它。
当thread 2 持有object 对象锁,并调用object.notify()方法后,唤醒thread 1,thread 1
重新获得object 对象锁继续执行。Object类方法说明:
三、Android对象内存结构
3.1 对象内存结构
一个类的实例对象内存主要由3部分组成:
1). 对象头:对象头包括kclass_和monitor_两个字段,其中kclass_ 存放指向类对象的指针,通过该指针可以找到该对象对应的类,monitor_ 用于存放对象运行时的标识数据,例如: GC 标志位、哈希码、锁状态等信息,后面详细分析。
2). 实例数据,该部分存放实例变量值,父类实例变量值在前,子类在后,且实例变量值按照如下顺序进行排序:
3).对齐填充,对象在内存中是按照8byte 对齐的,如果实例数据部分没有按照8byte对齐,则填充为8byte 对齐。
3.2 monitor_ 字段分析
monitor_ 字段定义在art/runtime/mirror/object.h,类型为uint32_t,主要有下面3个操作函数。
操作函数中SetLockWord和CasLockWord函数的入参或GetLockWord函数的返回值都包含LockWord 变量,对monitor_ 字段的操作是通过LockWord 的值进行的。
下面再来看LockWord 定义:
LockWord 类的定义在art/runtime/lock_word.h 文件中,从注释中可以看到LockWord的使用主要有4种状态,如下:
LockWord 的设计非常精妙,一个32 位数据的每一位都充分利用,而且很好的区分了不同状态。下面对各状态进行详细说明:
unlocked/thin 状态下31-30 bit 为00,默认状态下为unlocked 状态,当对象进行线程同步时变成thin lock 状态,27-16bit 记录了thin lock重入的次数,15-0 bit 记录了持有该thin lock的线程ID。
fat lock状态下31-30 bit 为01,当对象锁在thin lock状态,且有新的(非owner)线程与其竞争,经过适当的等待期(sched_yield调用、循环获取thin lock 状态)后依然无法拿到锁,则转换为fat lock 状态,并为该对象分配一个Monitor 资源。
hash state状态下31-30 bit 为10,在27-0 bit 存储对象的hash code,当在其它模式下,hash code 会存储在该对象关联的Monitor 对象中。
forwarding address state 状态下31-30 bit 为11,在concurrent copying GC 的copy 阶段,当一个对象被拷贝后,指向拷贝后的对象地址,当线程访问到该对象后,通过该转发地址,访问新的对象。
第29 位为mark bit,通过该bit位可以快速判断是否标记过,避免重复标记。
第28 位为read barrier bit,如果对象LockWorkd的该bit 被设置,则在访问该对象的成员时会进入慢速路径,判断对象是不是需要更新,如果需要更新,则返回拷贝后的对象地址。
四、对象锁代码分析
4.1 首先我们看一段代码
这段代码比较简单,主要有下面两个核心点:
1). 在主线程执行的过程中,用obj 对象进行线程同步,并调用obj.wait()函数,使线程阻塞在了obj 对象锁上等待唤醒。
2). main函数中创建匿名线程,该线程首先sleep 2000ms,然后唤醒阻塞在obj 对象锁上线程。
4.2编译TestDemo.java,命令如下:
1).Javac 将TestDemo.java 文件编译生成TestDemo*.class文件,java 编译过程中每个类会生成一个class 文件。
2).d8 命令将TestDemo*.class 文件通过编译、重构、重排、压缩、混淆后生成对应的dex (Dalvik Executable file)格式文件。
3).dexdump.exe命令可以查看dex 文件格式的详细信息,如校验信息、dex 头信息、生成dex 的CFG 信息、dex 的反汇编信息等,详细使用方法可以通过dexdump.exe –help 命令查看
通过dexdump.exe –d classes.dex 查看反汇编
其中run 方法指令信息如下:
main 函数的指令信息如下:
对部分指令解析如下:
对于dex 指令详细格式可以阅读google 的官方文档:
https://source.android.com/docs/core/runtime/dalvik-bytecode
本文重点分析monitor-enter、monitor-exit、Object.wait()、Object.notify()在虚拟机中的详细实现。
4.3.Object.wait() 流程分析
Object.wait() 的调用关系如下:
Object 类是所有类的父类,任何类中都可以调用public 的wait() 方法,最终调用到虚拟机的monitor.cc 文件的wait 静态方法,
首先构造了一个操作obj 的Handle对象h_obj,通过ObjectWaitStart 函数通知jvmti 调试系统发生了JVMTI_EVENT_MONITOR_WAIT 事件。
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,可以用来开发并监控虚拟机,可以查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。
首先获得h_obj 对象的LockWord 字段,lock_word.GetState()函数获得当前的锁状态,主要有下面几种情况:
1).hash 或unlocked 状态:
因为调用wait()方法必须持有对象锁,所以不会出现这两种状态,如果出现则抛出IllegalMonitorStateException 异常。
2).thin lock 状态:
当持有该对象锁的线程不是要wait 的线程,也抛出IllegalMonitorStateException 异常,当持有锁的线程与要wait 的线程一致,这时需要将thin lock inflate 为fat lock,inflate 的过程在monitor-enter 指令分析中分析。
当对象锁inflate 为fat lock 状态后,调用Monitor 对象的实例方法Wait让线程进入sleep 状态等待。
4.4 Object.notify() 流程分析
这里我们直接分析DoNotify 函数:
通过lock_word.GetState() 获得当前obj 对象的锁状态,主要有下面情况:
1) hash 或unlocked 状态 :
抛出IllegalMonitorStateException 异常。
2).thin lock 状态:
当持有该对象锁的线程不是要notify 的线程,也抛出IllegalMonitorStateException 异常,当持有锁的线程与要notify 的线程一致,这时说明没有需要通知唤醒的线程,直接返回。
3).fat lock 状态:
在Object.notify() 流程中参数notify_all 为false,则直接调用mon->Notify(self);通知唤醒等待线程。
4.5monitor-enter 流程分析
对于解释执行和机器码执行模式,最终都会调用到art/runtime/mirror/object-inl.h 文件Object 对象的MonitorEnter 函数。
下面来分析Monitor类的静态方法MonitorEnter 函数。
FakeLock 主要用于线程安全性检查,主要在编译期检测。
kExtraSpinIters 定义了当对象锁被其它线程持有且为thin lock 时,竞争线程循环获取锁的次数。
通过lock_word.GetState() 获取锁状态,当锁状态为unlocked 状态时,转换为thin lock 状态,并通过cas 操作更新lock count。
当锁状态为thin lock 状态时,首先获取锁的owner 线程id,如果owner id 与竞争线程id 一致,则有下面两种情况:
如果lock count加1小于等于(1<<12)-1(4095)时,将lock count+1 更新lock count。
如果lock count加1大于(1<<12)-1时(lock count 区域无法存储),则调用InflateThinLocked 函数对thin lock 进行膨胀。
Atrace* 相关的函数主要用于systrace 相关信息的打印,trylock 在这里为false。
当锁状态为thin lock 状态且锁的owner 线程id 与竞争线程id 不一致,则做一定的等待。
runtime->GetMaxSpinsBeforeThinLockInflation() 的值为50 ,也就是说执行100 次的循环判断锁状态后,再执行50次的sched_yield() 后还未获得锁资源,如果还未拿到锁,则对该锁进行膨胀。sched_yield() 会主动让出当前线程的执行权限,并在某个时间后恢复执行。
当锁状态已经是fat lock 状态,通过lock_word.FatLockMonitor(); 获取Monitor 对象,并通过Monitor 对象的Lock 函数让线程进入等待状态。
当锁状态已经是hash 状态时,直接对锁进行膨胀。
下面看锁膨胀的过程:
thin lock 的膨胀有两种情形:
1).lock count 的值超过了4095,这时锁的owner 为当前线程,即直接通过Inflate 函数膨胀
2).锁的owner不是当前线程,通过SuspendThreadByThreadId 暂停锁的owner 线程(主要是owner 线程和锁膨胀线程都需要访问对象的LockWord,避免竞态问题),然后通过Inflate 进行膨胀。膨胀完成后再唤醒锁的owner 线程。
再看Inflate 的过程:
通过MonitorPool::CreateMonitor函数获取一个Monitor 的对象m,并通过m->install(self)函数更新对象的LockWord字段,这时LockWord 字段信息包含fat lock 状态、GC 状态、MonitorId,然后将m 保存在monitor_list_ 中。
monitor_list_中存储了当前虚拟机使用的所有Monitor 对象。在GC 的过程中,通过该链表,访问到Monitor 依赖的对象。如果对象变成垃圾对象,则回收该Monitor,否则更新Monitor 依赖的对象信息。
MonitorId 用于唯一标识一个Monitor,生成的方法可以看monitor_pool.h 中的实现。
再看Monitor::Lock的过程:
该函数的实现较长,省去调试相关的代码。
首先介绍Monitor 中最重要的成员monitor_lock_ ,它是Mutex 的实例,通过该实例实现锁相关的核心逻辑。
TryLock 函数主要是通过Mutex的函数实现一定的自旋等待,并设置锁的状态为线程持有的状态。
monitor_lock_.ExclusiveLock(self);在Mutex 的ExclusiveLock函数中通过futex 系统调用实现了线程的阻塞,futex调用代码如下。
4.6 monitor-exit 流程分析
解释执行和机器码执行模式都会调用到MonitorExit 函数。
通过lock_word.GetState()获取LockWord 状态,当状态为hash 或unlocked 状态时,通过FailedUnlock函数抛出异常。
当LockWord 的状态为thin lock 状态时,有下面两种情况:
1).锁的owner 与当前线程不一致,则出错抛出异常。
2).锁的owner 与当前线程为同一线程,当锁有重入时,则将lock count -1,否则设置为unlocked 状态。
当LockWord 的状态为fat lock状态时,获取该对象关联的Monitor 对象,并调用Unlock 函数
在Unlock 函数中lock_count 为0,说明该线程不在持有该锁,通过
SignalWaiterAndReleaseMonitorLock 唤醒阻塞在该锁上的线程。
五、总结
本文简单的阐述了对象锁的使用方式,对象在内存中的结构,并对对象头中关键成员LockWord 进行了分析,最后介绍了synchronized、Object.wait()和Object.notify()在虚拟机中的实现流程。
参考文献:
深入理解Android Java虚拟机ART 邓凡平
深入理解java 虚拟机 周志明
深入java 虚拟机 bill venners
Android T 源码
长按关注内核工匠微信
Linux内核黑科技| 技术文章 | 精选教程