Java~深度剖析synchronized与ObjectMonitor、volatile和内存屏障


  • synchronized关键字最主要有以下3种应用方式,下面分别介绍
  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

  • 此时我们发现synchronize的实现离不开对象, 都是给对象在上锁, 所以我们研究一下对象

理解Java对象头与Monitor


  • 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

在这里插入图片描述

实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

头对象,它是实现synchronized的锁对象的基础,,一般而言,synchronized使用的锁对象是存储在Java对象头里的(也就是说synchronize的锁的本质不是这个对象, 而是锁的这个对象头里还有一个对象),jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark WordClass Metadata Address 组成

  • Mark Word

存储对象的hashCode、锁信息或分代年龄或GC标志等信息

  • Class Metadata Address

类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

  • 对象头的锁信息是与对象自身定义的数据没有关系的额外存储成本,除了上述列出的Mark Word默认存储信息外,还有如下可能存储的信息:
  1. 轻量级锁: 指向栈中记录的的指针, 以及其锁标志位

  2. **重量级锁:指向互相量(真正锁的那个对象)**的指针, 以及其锁标志位

  3. **偏向锁:**是否为偏向锁的标志位

  • 其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的

  • 主要分析一下重量级锁也就是通常说synchronized的对象锁,其中指向互斥量的那个指针是monitor对象(也称为管程或监视器锁)

  • Java中的每个对象的对象头都存在着一个 monitor对象与之关联,Java对象与其 monitor对象之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

  • 在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {

_header = NULL;

_count = 0; //记录个数

_waiters = 0,

_recursions = 0;

_object = NULL;

_owner = NULL;

_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet

_WaitSetLock = 0 ;

_Responsible = NULL ;

_succ = NULL ;

_cxq = NULL ;

FreeNext = NULL ;

_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表

_SpinFreq = 0 ;

_SpinClock = 0 ;

OwnerIsThread = 0 ;

}

  • ObjectMonitor中有两个队列,_WaitSet 和 _EntryList

  • 每个等待锁的线程都会被封装成ObjectWaiter对象

  • _owner指向持有ObjectMonitor对象的线程, 也就是正在访问同步代码块的线程

  • 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程检查_count为0, 进入 _Owner 区域并把monitor中的owner变量设置为当前线程, 同时monitor中的计数器count加1,此时也就意味着这个线程获取到了monitor对象, 当检查到owner不为null的时候就会执行owner指向的这个线程

  • 如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

  • 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201225145650667.png?x-oss-process=ima

必看视频!获取2024年最新Java开发全套学习资料 备注Java

ge/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NoYW5neGluZ3lh,size_16,color_FFFFFF,t_70)

可以得出结论

  1. monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因

  2. Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。

  3. synchronized是可重入的,所以不会自己把,自己锁死, 因为在monitor中,多次获取他只是计数器count++即可, 当释放的时候只要计数器count–到0 , 即可说明是无锁状态

  4. synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。而操作系统实现阻塞需要线程之间的切换, 从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。所以这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁

  • 对此我们不得不实现锁优化

Java虚拟机对synchronized的优化


  • 锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

偏向锁

  • 偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段

  • 经过研究发现,在**大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁. **

  • 偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

  • 对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失

  • 需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁

轻量级锁

  • 尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。

  • 轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”

  • 所以轻量级锁就是我们常说的乐观锁, 比如自旋锁

  • 轻量级锁所适应的场景是少量线程交替执行同步块的场合,如果存在大量线程同一时间访问同一锁的场合,导致自旋很长时间,就会导致轻量级升级为为重量级锁。

锁消除

  • 消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在编译时通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

锁粗化

  • 如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

总结

  • 所以在对比lock锁和synchronize的锁的时候就会发现很多不同之处
  1. 首先synchronize的是JVM实现的, 是一个关键字, 而lock是一个类, 是基于AQS实现的

  2. 要从数据结构上说, 他俩真的差不多,都是维护着一个变量和CAS操作的队列, 但是JVM的更细节, 他为了提高出队速度, 将一个队分成了俩个队列, 分别为_WaitSet 和 _EntryList, lock只维护了一个, 至于那个变量键值一模一样, 都是表示现在上锁的次数

  3. JVM对synchronized的做了优化, 引入了偏向锁和轻量级锁和锁消除和锁膨胀, 但是Lock则完全依靠系统阻塞挂起等待线程

  4. 当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多, 比如trylock

  5. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁

  6. 但是synchronized会自动释放锁, Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁

  • 所以Lock锁灵活适合大量同步的代码的同步问题,synchronized锁有优化适合代码少量的同步问题

volatile

最后

终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以判断是否获取到锁

  1. 但是synchronized会自动释放锁, Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
  • 所以Lock锁灵活适合大量同步的代码的同步问题,synchronized锁有优化适合代码少量的同步问题

volatile

最后

终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化

[外链图片转存中…(img-0MPebi6t-1716452220049)]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值