java基础Synchronized底层monitor

Synchronized修饰方法和代码块的区别

方法上加Synchronized是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在这里插入图片描述
Synchronized代码块,在反编译后是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
在这里插入图片描述

Monitor结构

synchronized底层使用monitor来控制锁的活动, wait(),notify(),notifyAll() 操作都与他有关,所以也必须在同步块能才能用。了解monitor中的各个属性值的含义,锁的竞争流程。ObjectMonitor主要数据结构如下:
_owner: 初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全。
_cxq: 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向列表)。cxq是一个临界资源,JVM通过CAS原子指令来修改cxq队列。修改前cxq的旧值填入了node的next字段,cxq指向新值(新线程)。因此cxq是一个后进先出的stack(栈)。
_EntryList:cxq队列中有资格成为候选资源的线程会被移动到该队列中。
_WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。
在这里插入图片描述

加锁

  1. 线程获取资源对象的锁,判断 _owner是否为空。这里操作是通过 CAS操作:比较和交换(Conmpare And Swap),比较新值和旧值的不同,替换。若owner能够在很短时间内 释放锁,则那些正在竞争的线程可以自旋等待一下,在owner线程释放锁后,竞争线程立即获得锁,从而避免系统阻塞。不过,当owner运行一段时间后后,线程不在自旋等待,则通过park将当
    前线程挂起,等待被唤醒。当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
  2. 如果 _owner为null ,直接把其赋值,指向自己, _owner = self ,同时把重入次数 _recursions = 1, 获取锁成功。
  3. 如果 _self == currentThread 和当前线程一致,说明是重入了, _recursions++ 即可
  4. 线程进入对象资源,处理。 同时等待当前线程的释放信号,期间一致持有对象资源的锁。

释放锁

1 当同步块内代码执行完,通过 ObjectMonitor::exit 退出
2 把线程插入到_EntryList中 _recursions–
3 再次从 _EntryList 中取出线程
4 调用unpark退出

流程总结

一个ObjectMonitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。
其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

在这里插入图片描述
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入(CAS,自旋)到cxq的队列的队首,然后调用park函数挂起当前线程。JDK的ReentrantLock底层也是用该方法挂起线程的。
当线程释放锁时,会从cxq或EntryList中挑选一个线程唤醒,就是图中的Ready Thread,假定被唤醒后会尝试获得锁,但synchronized是非公平的,这时新来的现象可能trylock。
如果线程获得锁后调用Object#wait方法,则会将线程加入到WaitSet中,当被Object#notify唤醒后,会将线程从WaitSet移动到cxq或EntryList中去。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁
synchronized的monitor锁机制和JDK的ReentrantLock与Condition是很相似的,ReentrantLock也有一个存放等待获取锁线程的链表,Condition也有一个类似WaitSet的集合用来存放调用了await的线程。

Synchronized和ReentrantLock的区别

原理弄清楚了,顺便总结了几点Synchronized和ReentrantLock的区别:

Synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
Synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
Synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
Synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁;
ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
Synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(上文有说),而ReentrantLock对于已经在等待的线程一定是先来的线程先获得锁;

常见问题

问:说一下synchronized的作用。
答:对于单一JVM来说,synchronized可以保证在并发情况下,同一时刻只有一个线程执行某个方法或某段代码,它可用于修饰方法或代码块,实现对同步代码的并发安全控制。

问:你刚刚说synchronized可用于修饰方法和代码块,他们有什么区别呢?
答:修饰方法在底层实现上会在方法访问标识中设置ACC_SYNCHRONIZED标示符,修饰代码块在底层实现上会使用monitorenter和monitorexit指令。

问:那你说一下修饰方法方式的底层实现原理?
答:反编译字节码文件,可以看到在方法的flags中设置了ACC_SYNCHRONIZED访问标识。每个对象都与一个monitor相关联,当且仅当monitor被线程持有时,monitor处于锁定状态。当方法执行时,线程将先尝试获取对象相关联的monitor所有权,然后再执行方法,最后在方法完成(无论是正常执行还是非正常执行)时释放monitor所有权。在方法执行期间,线程持有了monitor所有权,其它任何线程都无法再获得同一个对象相关联的monitor所有权。

上面的答案会引发面试官提问Java对象头和锁相关的问题,需要有心理准备。

问:那你再说一下修饰代码块方式的底层实现原理?
答:反编译字节码文件,可以看到在逻辑代码前添加了monitorenter指令,在逻辑代码尾添加了monitorexit指令。当方法执行时,当前线程执行monitorenter指令尝试获取对象相关联的monitor所有权时,如果此时这个monitor的计数器是0,那么当前线程持有该monitor,同时monitor计数器设置为1;如果当前线程已经持有了对象相关联的monitor所有权,只是想重新获取,那么继续持有该monitor,同时monitor计数器加1;如果有其它线程已经持有了对象相关联的monitor所有权,当前线程阻塞,直到monitor计数器为0,再次尝试获取所有权。方法正常执行或发生异常时,会执行monitorexit指令,释放monitor所有权,monitor计数器减1。

问:你刚刚说到了Monitor,能详细再说说吗?
答:Java虚拟机中,synchronized支持的同步方法和同步语句都是使用monitor来实现的。每个对象都与一个monitor相关联,当一个线程执行到一个monitor监视下的代码块中的第一个指令时,该线程必须在引用的对象上获得一个锁,这个锁是monitor实现的。在HotSpot虚拟机中,monitor是由ObjectMonitor实现,使用C++编写实现,具体代码在HotSpot虚拟机源码ObjectMonitor.hpp文件中。

查看源码会发现,主要的属性有_count(记录该线程获取锁的次数)、_recursions(锁的重入次数)、_owner(指向持有ObjectMonitor对象的线程)、_WaitSet(处于wait状态的线程集合)、_EntryList(处于等待锁block状态的线程队列)。

当并发线程执行synchronized修饰的方法或语句块时,先进入_EntryList中,当某个线程获取到对象的monitor后,把monitor对象中的_owner变量设置为当前线程,同时monitor对象中的计数器_count加1,当前线程获取同步锁成功。

当synchronized修饰的方法或语句块中的线程调用wait()方法时,当前线程将释放持有的monitor对象,monitor对象中的_owner变量赋值为null,同时,monitor对象中的_count值减1,然后当前线程进入_WaitSet集合中等待被唤醒。

问:你的回答中说到了锁,那一个对象的锁状态存在哪里?
答:Java对象的对象头Mark Word中。

问:对象头中包含哪些内容?
答:一部分是对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为“Mark Word”;一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

问:对对象头中的锁状态标识来说,synchronized属于哪一级别的锁?
答:重量级锁。

问:JVM对锁进行了哪些优化?
答:锁膨胀,

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Javasynchronized底层原理是基于对象监视器(Object Monitor)实现的。 每个Java对象都有一个与之关联的监视器锁(Monitor Lock),也称为内置锁(Intrinsic Lock)或互斥锁(Mutex Lock)。当一个线程试图进入一个synchronized方法或代码块时,它会尝试获取该方法或代码块所关联对象的监视器锁。 当一个线程成功获取锁后,它可以执行同步代码。其他线程如果想要获取同一个锁,就必须等待该线程释放锁。这样可以确保同一时间只有一个线程能够执行被锁定的代码,从而实现了线程安全。 底层实现中,监视器锁的获取和释放是通过底层操作系统的互斥原语来实现的。具体地说,当一个线程获取锁时,它会尝试将对象的监视器锁标记为已占用。如果锁已经被其他线程占用,那么获取锁的线程就会被阻塞,直到锁被释放。 在Java虚拟机中,对象监视器的实现通常包括一个等待队列(Waiting Queue)和一个通知队列(Notification Queue)。等待队列用于存放那些尝试获取锁但未成功的线程,而通知队列用于存放那些等待被唤醒的线程。 当一个线程释放锁时,它会将锁的状态设置为可用,并且从等待队列中选择一个线程进行唤醒,使其有机会获取锁。被唤醒的线程会进入到就绪状态,然后与其他线程竞争获取锁。 总之,Javasynchronized底层原理是通过对象监视器实现的,依赖于操作系统提供的互斥原语来实现线程同步和互斥访问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值