synchronized--对象头

在Hotspot源码的 sharedRuntime.cpp 中有synchronized的主要逻辑

加锁改变的是对象头的状态。对象在内存中由对象头、实例数据、填充字节组成。其中对象头是必不可少的,如果对象没有字段,则实例数据是空,填充字节是为了满足一个对象在内存中占用的空间是8字节的倍数,如果不满足,就进行填充,满足的话,填充字节也是空。

对象头

https://blog.csdn.net/lengxiao1993/article/details/81568130

https://www.cnblogs.com/LemonFive/p/11246086.html

可以通过unsafe类或者JOL工具类将对象头内存结构打印出来

Java对象头的组成#

Java对象的对象头由 mark word 、  klass pointer 组成,

mark word存储了同步状态、标识、hashcode、GC状态等等。

klass pointer存储对象的类型指针,该指针指向它的类元数据。值得注意的是,如果应用的对象过多,使用64位的指针将浪费大量内存。64位的JVM比32位的JVM多耗费50%的内存。我们现在使用的64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。

以64位操作系统为例,对象头存储内容图例。

age:Java GC标记位对象年龄。用4位存储,所以在JVM的GC机制中,之所以新生代进入老年代的条件是,对象在Survivor中经历15次GC,就是因为对象头的age是4位存储。

identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。使用hash与可偏向是互斥的,当使用了hash,锁就不可偏向,直接膨胀成轻量级锁。当对象被锁定时,该值会移动到线程Monitor中。

thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。https://blog.csdn.net/weixin_38239856/article/details/83183961,当一个线程退出后,新创建的线程可以复用原来的id。所以,当多线程是交替执行的时候,第一个线程拿到偏向锁,线程执行完后,后续线程有可能是拿到轻量级锁,也有可能是拿到偏向锁(threadID没变,因为threadID复用了)。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向线程Monitor的指针。

biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

lock:  锁状态标记位,该标记的值不同,整个mark word表示的含义不同。

偏向锁是JVM内置的一种锁机制,自JDK1.6后默认启用。启用参数: -XX:+UseBiasedLocking禁用参数: -XX:-UseBiasedLocking 取消延时加载偏向锁: -XX:BiasedLockingStartupDelay=0。偏向锁偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,那么持有偏向锁的线程无需再进行同步。很明显,当锁的竞争情况很少出现时,偏向锁就能提高性能,因为它比轻量级锁(如自旋锁)少了一步:CAS。

偏向机制被启用时,分配出来的对象状态是 ThreadId|Epoch|age|1|01, ThreadId 为空时标识对象尚未偏向于任何一个线程, ThreadId 不为空时, 对象既可能处于偏向特定线程的状态, 也有可能处于已经被特定线程占用完毕释放的状态, 需结合 Epoch 和其他信息判断对象是否允许再偏向(rebias)。

准备知识:

自旋锁与自适应自旋

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。 

 虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

 自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK1.6中已经变为默认开。自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。

JDK1.6中引入自适应的自旋锁,自适应意味着自旋的时间不在固定。而是有虚拟机对程序锁的监控与预测来设置自旋的次数。

自旋是在轻量级锁中使用的

 

 

synchronized 代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制

偏向锁(Biased Lock )
轻量级锁( Lightweight Lock)
重量级锁(Heavyweight Lock)
上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。

偏向锁

偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

流程讲解

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁逻辑

1.线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,依此判断此时对象锁是否处于无所状态或者偏向锁状态(匿名偏向锁);

2.然后判断偏向锁标志位是否为1,如果不是,则进入轻量级锁逻辑(使用CAS竞争锁),如果是,则进入下一步流程;

3.判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。

退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id

补充: 一个线程在执行完同步代码块以后, 并不会尝试将 MarkWord 中的 thread ID 赋回原值 。这样做的好处是: 如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功。

注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,而释放是指退出同步块时的过程。

4.如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块;

5.如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;

6.偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为重量级锁;

注:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。

7.如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁轻量级锁释放后,会转变为无锁状态。

8.如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID)

9.唤醒暂停的线程,从安全点继续执行代码

以上便是偏向锁的整个逻辑了。

批量重偏向与批量撤销

渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。 于是,就有了批量重偏向与批量撤销的机制。

解决场景:

批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。

批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

原理: 以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。

每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

1、批量重偏向和批量撤销是针对类的优化,和对象无关。

2、偏向锁重偏向一次之后不可再次重偏向。

3、当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

 

偏向锁的撤销(Revoke)
如上文提到的, 偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在偏向锁的获取过程中, 发现了竞争时, 直接将一个被偏向的对象“升级到” 被加了轻量级锁的状态。 

轻量级加锁的过程

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。

(2)拷贝对象头中的Mark Word复制到锁记录中。这时候线程堆栈与对象头的状态如图所示

图2.1


(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

在这里插入图片描述


5)如果这个更新操作失败了,说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁
 

偏向锁的批量再偏向(Bulk Rebias)机制

https://www.cnblogs.com/LemonFive/p/11248248.html
偏向锁这个机制很特殊, 别的锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。

那么作为开发人员, 很自然会产生的一个问题就是, 如果一个对象先偏向于某个线程, 执行完同步代码后, 另一个线程就不能直接重新获得偏向锁吗? 答案是可以, JVM 提供了批量再偏向机制(Bulk Rebias)机制

该机制的主要工作原理如下:

引入一个概念 epoch, 其本质是一个时间戳 , 代表了偏向锁的有效性。从前文描述的对象头结构中可以看到, epoch 存储在可偏向对象的 MarkWord 中。
除了对象中的 epoch, 对象所属的类 class C信息中, 也会保存一个 epoch 值。每当遇到一个全局安全点时, 如果要对 class C 进行批量再偏向, 则首先对 class C 中保存的 epoch 进行增加操作, 得到一个新的 epoch_new。然后扫描所有持有 class C 实例的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 仅将 epoch_new 的值赋给被锁定的对象中。
退出安全点后, 当有线程需要尝试获取偏向锁时, 直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。

重量级锁

重量级锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

轻量级锁在向重量级锁膨胀的过程中, 一个操作系统的互斥量(mutex)和条件变量( condition variable )会和这个被锁的对象关联起来。
具体而言, 在锁膨胀时, 被锁对象的 markword 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针

 

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
  • 重量级锁:有实际竞争,且锁竞争时间长。

另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。

如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。

重量级加锁的过程

存疑的问题
在锁膨胀的图例中, 线程 2 在线程 1 尚未释放锁时, 即将对象头修改为指向重量级锁的状态, 这个操作具体如何完成, 是否需要等待全局安全点?笔者尚未细究

轻量级锁的第一次获取时, 如果 CAS 操作失败, 按照 聊聊并发(二)Java SE1.6中的Synchronized 的描述, 会进行自旋的尝试。 但按照 Synchronization and Object Locking 的描述, 会去检测已加的锁是归属于自身线程, 没有提到自旋操作。 具体哪一种是正确的行为, 有待研究源码。

biasedLocking.cpp中的方法 revoke_and_rebias 存在 4 个条件分支, 其中笔者添加了注释的两个分支其主要逻辑已经清晰, 但未添加注释的两个分支具体逻辑笔者尚不清楚, 有待进一步研究

偏向所锁,轻量级锁都是乐观锁

重量级锁是悲观锁。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值