深入理解synchronized底层源码
前言
这篇文章从JVM源码分析synchronized的实现逻辑,这样才能更加对synchronized深度的认识。
进程:操作系统资源分配的基本单位。线程:cpu调度的基本单位(真实执行)
一、synchronized的使用场景
synchronized一般使用在下面这几种场景:
-
修饰代码块,指定一个加锁的对象,给对象加锁
public Demo1{ Object lock=new Object(); public void test1(){ synchronized(lock){ } } }
2.修饰静态方法,对当前类的Class对象加锁
public class Demo2 { //形式一 public void test1(){ synchronized(Synchronized.class){ } } //形式二 public void test2(){ public synchronized static void test1(){ } } }
3.修饰普通方法,对当前实例对象this加锁
public class Demo3 { public synchronized void test1(){ } }
二、JVM中,对象在内存中的布局
synchronized实现的锁是存储在Java对象头。所以要对synchronized深入理解,首先了解一下对象在内存中的布局怎样的?
在 JVM 中,对象在内存中分为这么三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
1、对象头(Header)两个部分组成:markOop或称为Mark Word和类元信息
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。源码如下:
class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4, //分代年龄 lock_bits = 2, //锁标识 biased_lock_bits = 1, //是否为偏向锁 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //对象的hashcode cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 //偏向锁的时间戳 };
类元信息:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2、实例数据:这部分主要是存放类的数据信息,父类的信息
3、对齐填充
三、synchronized底层从字节码聊起
首先,来看使用synchronized修饰的方法及方法块的小Demo案例,然后进行分析。由于synchronized的实现是在JVM层面的,我们要深入理解,那就是从字节码聊起了。
public class Demo03 { public static void main( String[] args ){ System.out.println( "hello Java" ); } //synchronized修饰普通方法 public synchronized void test1() { } //修饰代码块 public void test2() { synchronized (this) {} } }
接下打开终端运行javac Demo03.java
意思是将Demo03.java
变成Demo03.class
,然后再javap -v Demo03.class
这样可以查看字节码了如下。
观察上面字节码会发现
synchronized同步方法时:
如果修饰同步方法是通过的flag ACC_SYNCHRONIZED来完成的,也就是说一旦执行到这个方法,就会先判断是否有标志位,然后ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
synchronized修饰同步代码块时:
首先如果被synchronized修饰在方法块的话,是通过 monitorenter 和 monitorexit 这两个字节码指令获取线程的执行权的。当方法执行完毕退出以后或者出现异常的情况下会自动释放锁。
以上不管修饰哪一种:不管哪一种本质是对一个对象监视器(monitor)进行获取
在Java虚拟机执行到monitorenter指令时,1⃣️首先它会尝试获取对象的锁,如果该对象没有锁,或者当前线程已经拥有了这个对象的锁时,它会把计数器+1;然后当执行到monitorexit 指令时就会将计数器-1;然后当计数器为0时,锁就释放了。2⃣️如果获取锁 失败,那么当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
四、monitor到底是什么?
monitor它就是个监视器,底层源码是C++编写的。在hotspot虚拟机中,它是采用ObjectMonitor类来实现monitor的源码如下:
bool has_monitor() const { return ((value() & monitor_value) != 0); } ObjectMonitor* monitor() const { assert(has_monitor(), "check"); // Use xor instead of &~ to provide one extra tag-bit check. return (ObjectMonitor*) (value() ^ monitor_value); }
monitor实现在虚拟机的ObjectMonitor.hpp文件中的如下:
class ObjectMonitor { ... ObjectMonitor() { _header = NULL; //markOop对象头 _count = 0; _waiters = 0, //等待线程数 _recursions = 0; //线程重入次数 _object = NULL; //存储Monitor对象 _owner = NULL; //获得ObjectMonitor对象的线程 _WaitSet = NULL; //wait状态的线程列表 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 单向列表 FreeNext = NULL ; _EntryList = NULL ; //处于等待锁BLOCKED状态的线程 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; //监视器前一个拥有线程的ID } ...
五、深入synchronized底层源码
从 monitorenter和 monitorexit这两个指令来开始阅读源码,JVM将字节码加载到内存以后,会对这两个指令进行解释执行, monitorenter, monitorexit的指令解析是通过 InterpreterRuntime.cpp中的两个方法实现。
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else {//绕过偏向锁,直接进入轻量级锁 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); } assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object"); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END //%note monitor_1 IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); if (elem == NULL || h_obj()->is_unlocked()) { THROW(vmSymbols::java_lang_IllegalMonitorStateException()); } ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread); // Free entry. This must be done here, since a pending exception might be installed on // exit. If it is not cleared, the exception handling code will try to unlock the monitor again. elem->set_obj(NULL); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END
注:
//JavaThread 当前获取锁的线程 //BasicObjectLock 基础对象锁
UseBiasedLocking是在JVM启动的时候,是否启动偏向锁的标识
1、如果支持偏向锁,则执行 ObjectSynchronizer::fast_enter的逻辑
ObjectSynchronizer::fast_enter实现在 synchronizer.cpp文件中,代码如下
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { if (UseBiasedLocking) {//判断是否开启锁 if (!SafepointSynchronize::is_at_safepoint()) {//如果不处于全局安全点 //通过`revoke_and_rebias`这个函数尝试获取偏向锁 BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {//如果是撤销与重偏向直接返回 return; } } else {//如果在安全点,撤销偏向锁 assert(!attempt_rebias, "can not rebias toward VM thread"); BiasedLocking::revoke_at_safepoint(obj); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } slow_enter (obj, lock, THREAD) ; }
小结:
再次检查偏向锁是否开启
当处于不安全点时,通过 revoke_and_rebias尝试获取偏向锁,如果成功则直接返回,如果失败则进入轻量级锁获取过程
revoke_and_rebias这个偏向锁的获取逻辑在 biasedLocking.cpp中
如果偏向锁未开启,则进入 slow_enter获取轻量级锁的流程
2、如果不支持偏向锁,则执行 ObjectSynchronizer::slow_enter逻辑,绕过偏向锁,直接进入轻量级锁,该方法同样位于 synchronizer.cpp文件中
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); if (mark->is_neutral()) {//如果当前是无锁状态, markword的 //直接把mark保存到BasicLock对象的_displaced_header字段 lock->set_displaced_header(mark); //通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } // Fall through to inflate() ... } //如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要争抢锁 else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); lock->set_displaced_header(NULL); return; } #if 0 // The following optimization isn't particularly useful. if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { lock->set_displaced_header (NULL) ; return ; } #endif //代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过`inflate`进行膨胀升级为重量级锁 lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }
六、锁的优化
大家都知道JDK1.6之前,synchronized是一个重量级锁,效率比较低。所以官方在JDK1.6开始,为了减少获得锁和释放锁带来的性能消耗,对synchronized进行了优化,引入了 偏向锁和 轻量级锁的概念。
无锁状态--->偏向锁状态--->轻量级锁状态--->重量级锁状态四种状态,锁的状态会随着锁竞争的情况逐步升级,且锁升级是不可逆的。
1、偏向锁
偏向锁是指如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入同步,不需要再次进行抢占锁和释放锁的操作。
该过程是采用CAS客观锁操作的,意思就是一个线程获取偏向锁时,如果没有其他线程来竞争锁,那么持有偏向锁的线程再次进入,虚拟机就不进行任何同步操作了,对标志位加1即可;如果不同线程来竞争锁,CAS会失败,这样获取锁就失败了 。
JDK 1.5偏向锁是关闭的,开启参数xx:-UseBiasedLocking=false,JDK1.6后默认开启。
2、偏向锁的获取
当一个线程访问同步块获取锁时,会在对象头(Mark Word)和栈帧中的锁记录里存储偏向锁的线程ID,表示哪个线程获得了偏向锁。
获取过程:
1)首先根据锁的标志判断是不是处于偏向锁的状态
2)如果是偏向锁状态,就通过CAS操作将自己的线程ID写入到MarkWord,如果CAS操作成功,说明当前线程获取到偏向锁,然后就继续执行同步代码块。如果CAS失败,那就是意味着获取锁失败。
3)如果当前不是偏向锁,那它会去检测MarkWord中存储的线程ID和当前访问的线程的线程ID是否相等,如果相等,就说明当前线程已经是获取偏向锁,然后直接执行同步代码;如果不相等,说明当前偏向锁被其他线程获取,需要撤销偏向锁。
3、撤销偏向锁
获取偏向锁的线程才会释放偏向锁,撤销偏向锁的过程需要等待一个全局安全点(也就是等待获取偏向锁的线程都停止字节码执行)。
撤销偏向锁的过程:
1)首先,判断获取偏向锁的线程否为存活状态
2)如果线程已存亡,那就直接把Mark Word设置为无锁状态
3)如果线程还存活,当达到全局安全点时,获取的偏向锁的线程会被挂起,然后接着偏向锁升级为轻量级锁,最后唤醒被阻塞在全局安全点的线程继续往下执行同步代码
4、轻量级锁
当多个线程竞争偏向锁时,会发生偏向锁的撤销,偏向锁撤销无非两种状态:
1)没有获取偏向锁的无锁状态
2)没有获取偏向锁的有锁状态
5、轻量级锁加锁过程
1)如果这个对象是无锁的,JVM就会在当前线程的栈帧创建用于存储锁记录的空间(LockRecord),用来将对象头中的Mark Word复制到锁记录中的
2)然后JVM采用CAS将对象头中的Mark Word替换为指向锁记录的指针
3)替换成功,说明当前线程获得轻量级锁;替换失败,说明存在其他线程竞争锁。那么当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁
自旋,防止线程被挂起,一旦可以获取资源,就直接尝试成功,如果超出阈值,还没有获取锁,那么升级为重量级锁。(自旋锁默认是10次,-XX:PreBlockSpin可以修改)
6、重量级锁状态
一旦锁升级为重量级锁,就不会再恢复到轻量级锁状态。当锁处于重量级锁状态,其他线程尝试获取锁时,都会被阻塞,也就是 BLOCKED状态。当持有锁的线程释放锁之后会唤醒这些现场,被唤醒之后的线程
ref:https://blog.csdn.net/realize_dream/article/details/106968443