详细介绍synchronized和Object的关键方法和虚拟机实现原理

描述

一、前言

编程过程中经常会遇到线程的同步问题,Java 中对同步问题的解决方案比较多(synchronized、JUC、原子操作、volatile、条件变量等),其中synchronized 最方便、简单易用,也是java 编程中使用最多的临界区保护方案。本文主要讲述对象锁的相关知识,详细介绍synchronized 和Object 的关键方法的虚拟机实现原理。

二、Java 对象锁的使用方式

2.1 实例方法的同步

JAVA

synchronized 修饰实例方法,该同步仅对当前对象的该方法起作用,同一时间只能有一个线程可以进入该对象的此方法。对于不同对象的此函数,无法做到互斥保护。

2.2 静态方法的同步

JAVA

synchronized 修饰静态方法,该同步对当前类对象的该方法起作用,同一时间只能有一个线程可以进入该方法。

2.3 代码块的同步

JAVA

在大多少情况下,并不需要对整个方法进行保护,当synchronized 修饰代码块时,该代码块的访问依赖于object 对象锁的互斥访问,同一时间只能有一个线程持有object 对象锁。

更准确的来讲,synchronized 关键字是依赖于对象锁而生效的,每个synchronized 同步块开始的地方都会生成monitor-enter obj指令,同步块结束的地方生成monitor-exit obj 的指令,其中obj 为用于控制互斥访问的对象。同一时间只能有一个线程持有obj 的对象锁。在2.1 中synchronized 依赖的是实列对象,2.2 中synchronized 依赖的是类对象,2.3 中synchronized 依赖的是object 对象。

当一个对象控制多个代码块时,多个代码块也是互斥访问,如下面代码:

JAVA

代码块①和代码块② 虽然在两个函数中,但是synchronized 依赖的对象都为object,这两个代码块也是互斥访问。

2.4 Object  wait() 和notify() 使用方法

Object 作为所有类的基类,都实现了object方法。典型的用法如下:

JAVA

thread 1持有object 对象锁,并调用object.wait() 方法后,则该线程进入WAITING状态,并释放object 对象锁,等待其它线程来唤醒它。

当thread 2 持有object 对象锁,并调用object.notify()方法后,唤醒thread 1,thread 1

重新获得object 对象锁继续执行。Object类方法说明:

JAVA

三、Android对象内存结构

3.1 对象内存结构

JAVA

一个类的实例对象内存主要由3部分组成:

1). 对象头:对象头包括kclass_和monitor_两个字段,其中kclass_ 存放指向类对象的指针,通过该指针可以找到该对象对应的类,monitor_ 用于存放对象运行时的标识数据,例如: GC 标志位、哈希码、锁状态等信息,后面详细分析。

2). 实例数据,该部分存放实例变量值,父类实例变量值在前,子类在后,且实例变量值按照如下顺序进行排序:

JAVA

3).对齐填充,对象在内存中是按照8byte 对齐的,如果实例数据部分没有按照8byte对齐,则填充为8byte 对齐。

3.2 monitor_ 字段分析

monitor_ 字段定义在art/runtime/mirror/object.h,类型为uint32_t,主要有下面3个操作函数。

JAVA

操作函数中SetLockWord和CasLockWord函数的入参或GetLockWord函数的返回值都包含LockWord 变量,对monitor_ 字段的操作是通过LockWord 的值进行的。

下面再来看LockWord 定义:

LockWord 类的定义在art/runtime/lock_word.h 文件中,从注释中可以看到LockWord的使用主要有4种状态,如下:

JAVA

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 首先我们看一段代码

JAVA

这段代码比较简单,主要有下面两个核心点:

1). 在主线程执行的过程中,用obj 对象进行线程同步,并调用obj.wait()函数,使线程阻塞在了obj 对象锁上等待唤醒。

2). main函数中创建匿名线程,该线程首先sleep 2000ms,然后唤醒阻塞在obj 对象锁上线程。

4.2编译TestDemo.java,命令如下:

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 方法指令信息如下:

JAVA

main 函数的指令信息如下:

JAVA

对部分指令解析如下:

JAVA

本文重点分析monitor-enter、monitor-exit、Object.wait()、Object.notify()在虚拟机中的详细实现。

4.3.Object.wait() 流程分析

Object.wait() 的调用关系如下:

JAVA

Object 类是所有类的父类,任何类中都可以调用public 的wait() 方法,最终调用到虚拟机的monitor.cc 文件的wait 静态方法,

JAVA

首先构造了一个操作obj 的Handle对象h_obj,通过ObjectWaitStart 函数通知jvmti 调试系统发生了JVMTI_EVENT_MONITOR_WAIT 事件。

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,可以用来开发并监控虚拟机,可以查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。

JAVA

首先获得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() 流程分析

JAVA

这里我们直接分析DoNotify 函数:

JAVA

通过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 函数。

JAVA

下面来分析Monitor类的静态方法MonitorEnter 函数。

JAVA

FakeLock 主要用于线程安全性检查,主要在编译期检测。

kExtraSpinIters 定义了当对象锁被其它线程持有且为thin lock 时,竞争线程循环获取锁的次数。

JAVA

通过lock_word.GetState() 获取锁状态,当锁状态为unlocked 状态时,转换为thin lock 状态,并通过cas 操作更新lock count。

JAVA

当锁状态为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。

JAVA

当锁状态为thin lock 状态且锁的owner 线程id 与竞争线程id 不一致,则做一定的等待。

runtime->GetMaxSpinsBeforeThinLockInflation() 的值为50 ,也就是说执行100 次的循环判断锁状态后,再执行50次的sched_yield() 后还未获得锁资源,如果还未拿到锁,则对该锁进行膨胀。sched_yield() 会主动让出当前线程的执行权限,并在某个时间后恢复执行。

JAVA

当锁状态已经是fat lock 状态,通过lock_word.FatLockMonitor(); 获取Monitor 对象,并通过Monitor 对象的Lock 函数让线程进入等待状态。

JAVA

当锁状态已经是hash 状态时,直接对锁进行膨胀。

下面看锁膨胀的过程:

JAVA

thin lock 的膨胀有两种情形:

1).lock count 的值超过了4095,这时锁的owner 为当前线程,即直接通过Inflate 函数膨胀

2).锁的owner不是当前线程,通过SuspendThreadByThreadId 暂停锁的owner 线程(主要是owner 线程和锁膨胀线程都需要访问对象的LockWord,避免竞态问题),然后通过Inflate 进行膨胀。膨胀完成后再唤醒锁的owner 线程。

再看Inflate 的过程:

JAVA

通过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的过程:

该函数的实现较长,省去调试相关的代码。

JAVA

首先介绍Monitor 中最重要的成员monitor_lock_ ,它是Mutex 的实例,通过该实例实现锁相关的核心逻辑。

TryLock 函数主要是通过Mutex的函数实现一定的自旋等待,并设置锁的状态为线程持有的状态。

monitor_lock_.ExclusiveLock(self);在Mutex 的ExclusiveLock函数中通过futex 系统调用实现了线程的阻塞,futex调用代码如下。

JAVA

4.6 monitor-exit 流程分析

解释执行和机器码执行模式都会调用到MonitorExit 函数。

JAVA

通过lock_word.GetState()获取LockWord 状态,当状态为hash 或unlocked 状态时,通过FailedUnlock函数抛出异常。

JAVA

当LockWord 的状态为thin lock 状态时,有下面两种情况:

1).锁的owner 与当前线程不一致,则出错抛出异常。

2).锁的owner 与当前线程为同一线程,当锁有重入时,则将lock count -1,否则设置为unlocked 状态。

JAVA

当LockWord 的状态为fat lock状态时,获取该对象关联的Monitor 对象,并调用Unlock 函数

JAVA

在Unlock 函数中lock_count 为0,说明该线程不在持有该锁,通过SignalWaiterAndReleaseMonitorLock 唤醒阻塞在该锁上的线程。

五、总结

本文简单的阐述了对象锁的使用方式,对象在内存中的结构,并对对象头中关键成员LockWord 进行了分析,最后介绍了synchronized、Object.wait()和Object.notify()在虚拟机中的实现流程。






审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分