synchronized锁的膨胀

tech2022-12-24  62

1.在编译层面,synchronized关键字编译成class文件后,会被再次编译成JVM级别的汇编语言,

2.在汇编语言中synchronized会被编译成monitorenter,

3.当synchronized修饰的临界区代码发生异常,会自动释放锁,因为在java-c的汇编级别已经做了monitorexit处理,monitorexit就是拿来用于解锁的

4.monitorenter这个关键字,JVM会从c++的代码bytecodeinterpreter.app(字节码解析器)类中去解析。

5.在对象头中,如果当前锁对象的状态为轻量锁,那么它前62为存储的是lock record的指针,这个指针存储在对象头的mark word中

6.这个指针指向我们当前线程栈中的一个对象,这个对象本名叫BasicObjectLock,业内将其称为lock record,原因是JVM内部将其翻译成lock record

7.lock record有两个属性,一个是_displaced_header,另一个是object_reference, _displaced_header存储的是原本mark word中的信息,object_reference中存储的是当前锁对象的引用地址

8.当JVM遇到monitorenter这个命令时,它的当前状态是:不管当前是偏向锁,还是轻量锁还是什么,都会首先在当前线程的线程栈中,创建一个lock record,此时object_reference存储对象引用,_displaced_header里面还未存储mark word

bytecodeinterpreter.app类中的关键操作步骤:

1.首次加锁(101无锁可偏向未偏向): 1.获取当前锁对象 2.把当前锁对象引用存储到lock record的object_reference 3.拿到锁对象的mark work 4.判断JVM是否将偏向禁用 5.如果被禁用,直接走轻量锁 6.如果没有禁用,并且通过mark work计算出当前为无锁可偏向状态,把mark work的线程id改成当前线程id 7.产生一个epoch时间戳 8.拿到锁,返回

已经偏向模式(101无锁可偏向已偏向): 1.获取当前锁对象 2.把当前锁对象引用存储到lock record的object_reference 3.拿到锁对象的mark work 4.判断JVM是否将偏向禁用 5.如果被禁用,直接走轻量锁 6.如果没有禁用,那么将mark word拿出来计算,计算后会得出一个值(值名:anticipated_bias_locking_value),然后判断值是否等于0,如果等于0,则表示当前线程是偏向自己的,则将success= true。 7.当锁对象的状态成为偏向锁后,mark work中会产生一个epoch值,这个epoch值是一个时间戳,用于记录我们当前存储在mark work中的线程id是否过期。将success= true后,拿到mark work中的epoch值,进行判断当前线程id是否过期 8.如果没有过期,则拿到锁,直接返回。

3.轻量锁的加锁逻辑(001): 1.获取当前锁对象 2.把当前锁对象引用存储到lock record的object_reference 3.将mark work改成无锁状态 4.拿到锁对象的mark work 5.判断JVM是否将偏向禁用 6.如果被禁用,直接走轻量锁(指的是下一步 ) 7.如果没有禁用,那么将mark word拿出来计算,计算后会得出一个值(值名:anticipated_bias_locking_value),然后判断值是否等于0,如果不等于0,则将success=false; 8.success=false后,开始执行(!success){}里面的代码 9.首先产生一个无锁的值,也就是00000000000001等,没有线程id,且是001(无锁不可偏向),值名:displaced,将对象头中mark work中的值替换成displaced

10.进行cas替换,把displaced存到lock record的_displaced_header中,此时mark work的值空了,再将mark work的前56位值替换成指向当前线程栈的lock record的指针,此时加锁成功。并且将mark work中的锁标志位的001(无锁不可偏向)改成了000(有锁不可偏向) 11.等解锁的时候在把lock record的_displaced_header中无锁的值写回mark work中(此时mark work就变成了无锁不可偏向)。

什么情况下轻量锁会加锁失败,膨胀成重量锁? 答:例如t2进行到第9步,还未进行cas,此时cpu将时间片切换给了t3,t3进行了cas,并且将mark work改成了000,此时时间片切给t2,t2开始进行cas,但是mark work的值已经被t3改成了000,所以t2加锁失败,膨胀成重量锁。

4.重量锁的加锁逻辑(010) 1.判断是否开启了偏向模式,原因是为了做偏向的撤销(有损性能) 2.如果没有开启偏向模式,则会执行slow_enter的代码 1.首先判断是不是无锁状态 2.如果是膨胀得情况下,当前肯定是有锁得 3.如果当前有锁,则调用inflate方法膨胀成为重量锁 4.膨胀成为重量锁后会返回一个ObjectSynchronize 5.ObjectSynchronize里面会有一个Objectmonitor类 6.然后调用Objectmonitor(里面维护了一个队列)里面得entor方法 entor方法: 1.看当前持有锁得线程是否为null,是的话直接获取锁,所以这个是非公平锁得直接证明 2.如果当前线程不为null,则再次自旋一次 3.自旋一段时间后如果还是拿不到锁,然后就进入一个for(;;)死循环,死循环内部调用一个EnterI方法,EnterI方法主要会把线程封装成一个node,让线程入队,此时入队还未park,会再次执行一次cas,自旋尝试拿锁,如果还是拿不到就会执行_parkEvent得park方法,_parkEvent继承了PlatformEvent(与平台有关,也就是当前操作系统) 4.park方法内部调用了pthread_lock_mutex

偏向延迟: 延迟4秒钟,是因为jvm启动的时候禁用了4秒钟,为什么要禁用四秒钟?是因为jvm在启动过程中,用了很多的synchronized的关键字,而且因为代码是sun公司自己写的,所以他知道自己的的锁不会是偏向锁,为了不做偏向撤销(因为偏向撤销影响性能)所以就禁用了4秒偏向,为什么是4秒?因为sun公司觉得四秒后jvm差不多启动完成了

5. 解锁逻辑 1.首先判断是不是偏向锁 2.如果不是偏向锁就先把mark work中的值替换回来,也就是把mark work的值变成00000000001等 3.如果是偏向锁,就直接将线程中的lock record设为null

重入锁 5.轻量锁的重入 1.首先不管是偏向锁还是轻量锁,都会在线程栈中创建一个lock record 2.如果判断是重入的话,会再次在本线程中再次创建一个lock record,也就是重入几次,创建几个lock record,,但是lock record里面的_displaced_header和object_reference都是空值,解锁时就将lock record设置为null 3.且加锁会失败

6.重量锁的重入

性能: 1. 偏向锁和轻量锁的性能比较主要是体现在解锁过程中,轻量锁的解锁步骤比偏向锁稍复杂一些。 2. 偏向锁的性能为何高,例如t1拿到锁时,此时锁对象本来是无锁可偏向的状态,当t1拿到锁之后,将锁对象状态改成了有锁可偏向(101),并且在对象头的mark work中存储了t1的线程id,等t1释放锁后,如果再次拿到这把锁,例如下图场景(主线程两次拿到同意把锁),那么此时就是偏向锁,在bytecodeinterpreter.app只需要做一个简单的判断,判断这个锁是否偏向t1线程,如果是得话,就将success=true,然后拿到mark work中的epoch值,用来判断线程id是否过期,如果没有过期,则拿到锁,直接返回。

敲黑板: 1.偏向锁释放之后,对象头里面的状态还是101,还是偏向锁的状态。

2.轻量锁释放锁之后,锁状态会成为001,一旦变成轻量锁,99%的情况就不会再回到偏向锁

最新回复(0)