synchronized源码解析

synchronized:

synchronized是解决线程安全的问题。

在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。

对象头

Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

img

实例数据

这部分主要是存放类的数据信息,父类的信息。

对其填充

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

synchronized 修饰方法:生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,也叫隐式同步。

synchronized 修饰代码块:加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令,每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当一个线程获执行monitorenter后,该计数器自增1;当同一个线程执行monitorexit指令的时候,计数器再自减1。当计数器为0的时候,monitor将被释放.也叫显式同步。

Monitor(管程/监视器)

monitor 在操作系统层面的专业术语叫“管程”,管程指的是管理共享变量和对共享变量的操作过程,让它们支持并发。

MESA模型

mesa是属于解决管程相关的一种模型,也是被广泛使用最多的一种。

monitor的主要数据结构

hotspot ObjectMonitor.hpp 文件中

 //ObjectMonitor.hpp
 // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL; //对象头
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;    //锁的重入次数
    _object       = NULL;
    _owner        = NULL; //标识拥有该monitor的线程(当前获取锁的线程)
    _WaitSet      = NULL; //等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

HotSpot源码之Monitor竞争

目标: 通过JVM虚拟机源码分析synchronized多个线程抢夺锁,拿到锁之后要干什么?

调用objectMonitor.cpp文件中的 ObjectMonitor::enter。

总结:

1.通过CAS尝试把monitor的owner字段设置为当前线程。
2.如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行
recursions ++ ,记录重入的次数。
3.获取锁失败的线程,则【等待】锁的释放。

一句话总结:自旋拿锁、拿到+1 、拿不到等待(竞争队列)。

HotSpot源码之Monitor等待

目标: 通过JVM虚拟机源码分析synchronized拿不到锁的线程他们都去干什么了?

调用objectMonitor.cpp文件中的 ObjectMonitor::enter。

总结

1.竞争失败的线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ(竞争队
列)。

2.在for循环中,通过CAS把node节点push到_cxq列表中,(竞争队列)。

3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当
前线程挂起,等待被唤醒。

4.当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。

一句话总结:没拿到,尝试拿一次、在自旋去拿、实在拿不到就去竞争队列、等待唤醒。

HotSpot源码之Monitor释放

目标: 通过JVM虚拟机源码分析synchronized拿到锁的线程最后是怎么释放锁的?

ObjectMonitor::exit。

总结
1、先进入减减操作,直到为0。
2、为0后,唤醒竞争队列的线程。
3、唤醒线程后,继续争夺锁,循环前面的步骤(锁竞争-----等待----释放)。
一句话总结:释放后,进入减减操作、直到为0然后唤醒队列,让他们去争夺锁,循环前面步骤。

monitor 重量级锁

monitor c++源码中频繁调用linux内核cas函数、以及jvm中的用户函数,涉及到内核态到用户态的切换,会占用大量的系统资源。

1.6之前是重量级锁,没错,但是他重量的本质,是ObjectMonitor调用的过程,以及Linux内核的复杂运行机制决定的,大量的系统资源消耗,所以效率才低。

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
synchronized 锁升级原理:

在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁(只要有另一个竞争线程就升级),通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

img

1 偏向锁

为什么要引入偏向锁?

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁的取消:

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

2 轻量级锁

轻量级锁(按次数自旋,也叫自旋锁)开启自旋 -XX:+UseSpinning默认自旋10次,修改自旋次数-XX:PreBlockSpin=20

为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

轻量级锁什么时候升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只尘世中迷途小书童

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值