JVM同步方法之偏向锁

作者: 半栈工程师 | 来源:发表于2018-03-18 03:56 被阅读9次

    其实很早之前通过一些资料,就对偏向锁稍微有些了解,周六准备看看Hotspot中关于偏向锁的实现,本以为应该畅通无阻,没想到处处都是拦路虎,细节比较多,真是硬着头皮看了一整天,才大概懂了点。笔者还在不断学习,只是想把自己的笔记分享出来,理解能力有限,可能有不正确的地方,还望指正,别让我误导了他人😭。

    一:锁的表示

    Java里的锁,主要都是对对象进行加锁,如普通的synchronized非静态方法,就是对当前执行方法的对象进行加锁。那么怎么对对象进行加锁呢?对象的锁其实主要就是通过对象头的markOop进行表示的。markOop其实不是一个对象,只是一个字长的数据,在32为机器上,markOop为32个位,在64位上为64个位。markOop中不同的位区域存储着不同的信息,但是需要注意的一点是,markOop每个位区域表示的信息不是一定的,在不同状态下,markWord中存着不同的信息。接下来盗图一张:

    584866-20170420091115212-1624858175.jpg

    由上图可知在markWord在对象的不同状态下,会有5种表示形式。

    二:何为偏向锁

    很多情况下,一个锁对象并不会发生被多个线程访问得情况,更多是被同一个线程进行访问,如果一个锁对象每次都被同一个线程访问,根本没有发生并发,但是每次都进行加锁,那岂不是非常耗费性能。所以偏向锁就被设计出来了。

    偏向,也可以理解为偏心。当锁对象第一次被某个线程访问时,它会在其对象头的markOop中记录该线程ID,那么下次该线程再次访问它时,就不需要进行加锁了。但是这中间只要发生了其他线程访问该锁对象的情况,证明这个对象会发生并发,就不能对这个对象再使用偏向锁了,会进行锁的升级,这是后话,我们这里还是主要讨论下偏向锁。

    三:源码探究

    我们就以synchronized方法为入口吧。

    之前在《JVM方法执行的来龙去脉》中提到过,JVM执行方法最后会以对应的entry_point例程作为入口。entry_point例程不仅会进行java方法栈帧的创建,如果是同步方法,还会进行加锁:

    address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
      ......
      if (synchronized) {
        // Allocate monitor and lock method
        lock_method();
      } else {
        ......
      }
      // 下面会开始执行方法的字节码
      ......
    
    }
    
    

    可见在执行方法的字节码之前,对于同步方法,entry_point例程插入了一道关卡:lock_method():

    void TemplateInterpreterGenerator::lock_method() {
    
      .......
      // get synchronization object
      {
        Label done;
        __ movl(rax, access_flags);
        __ testl(rax, JVM_ACC_STATIC);
        // get receiver (assume this is frequent case)
        // 局部变量表中第一个变量,存放着即将锁的对象指针,移动到rax中
        __ movptr(rax, Address(rlocals, Interpreter::local_offset_in_bytes(0)));
        __ jcc(Assembler::zero, done);
        __ load_mirror(rax, rbx);
    
        __ bind(done);
      }
    
      // add space for monitor & lock
      // 在当前栈帧中分配一个空间,用于分配一个BasicObjectLock对象
      __ subptr(rsp, entry_size); // add space for a monitor entry
      __ movptr(monitor_block_top, rsp);  // set new monitor block top
      // store object
      // 将要锁的对象指针移动到分配的BasicObjectLock中的obj变量
      __ movptr(Address(rsp, BasicObjectLock::obj_offset_in_bytes()), rax);
      const Register lockreg = NOT_LP64(rdx) LP64_ONLY(c_rarg1);
      // 将分配的BasicObjectLock的指针移动到lockreg寄存器中
      __ movptr(lockreg, rsp); // object address
      // 加锁
      __ lock_object(lockreg);
    }
    
    

    在上面的lock_method()中,会在当前方法栈帧中分配一段空间,用于分配一个BasicObjectLock对象,这个对象主要干两件事,一是记录将要锁的对象指针,而是用一个字长的空间,复制锁对象的markOop。现在我们可能不知道这么做是为什么,但是后面就会清楚了。主要上面最后一步,调用了lock_object()进行加锁:

    void InterpreterMacroAssembler::lock_object(Register lock_reg) {
      ......
      // 如果使用重量级锁,直接进入InterpreterRuntime::monitorenter()执行
      if (UseHeavyMonitors) {
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
                lock_reg);
      } else {
        Label done;
        // cmpxchg其实就是CAS操作,必须使用rax寄存器作为老数据的存储。
        const Register swap_reg = rax; // Must use rax for cmpxchg instruction
        const Register tmp_reg = rbx; // Will be passed to biased_locking_enter to avoid a problematic case where tmp_reg = no_reg.
        const Register obj_reg = LP64_ONLY(c_rarg3) NOT_LP64(rcx); // Will contain the oop
    
        ......
        Label slow_case;
    
        // Load object pointer into obj_reg
        movptr(obj_reg, Address(lock_reg, obj_offset));
        //如果虚拟机参数允许使用偏向锁,那么进入biased_locking_enter()中
        if (UseBiasedLocking) {
          // lock_reg :存储的是分配的BasicObjectLock的指针
          // obj_reg :存储的是锁对象的指针
          // slow_case :即InterpreterRuntime::monitorenter();
          // done :标志着获取锁成功。
          // slow_case 和 done 也被传入,这样在biased_locking_enter()中,就可以根据情况跳到这两处了。
          biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
        }
        ......
        ......
    
        // 直接跳到这,需要进入InterpreterRuntime::monitorenter()中去获取锁。
        bind(slow_case);
        // Call the runtime routine for slow case
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
                lock_reg);
        // 直接跳到这表明获取锁成功,接下来就会返回到entry_point例程进行字节码的执行了。
        bind(done);
      }
    }
    
    

    上面可知,如果虚拟机参数允许使用偏向锁,那么会进入biased_locking_enter()中,biased_locking_enter()这个方法涉及到了很多细节,说实话在不了解这些细节的情况下直接看代码,简直是一头雾水。接下来还是一边看代码一边去讲解细节吧。

    四:偏向锁的获取

    biased_locking_enter()也比较长,就不直接贴方法块了,一步步分析比较好。

    1:判断锁对象是否为偏向锁状态

      // mark_addr:锁对象头中的markOop指针。
      Address mark_addr      (obj_reg, oopDesc::mark_offset_in_bytes());
      NOT_LP64( Address saved_mark_addr(lock_reg, 0); )
    
      if (PrintBiasedLockingStatistics && counters == NULL) {
        counters = BiasedLocking::counters();
      }
    
      Label cas_label;
      int null_check_offset = -1;
      // 如果swap_reg中没存mark_addr,那么就先将mark_addr存入swap_reg中。
      if (!swap_reg_contains_mark) {
        null_check_offset = offset();
        movptr(swap_reg, mark_addr);
      }
      // 将对象的mark_addr,即markOop指针移入tmp_reg中
      movptr(tmp_reg, swap_reg);
      // 将tmp_reg和biased_lock_mask_in_place进行与操作,biased_lock_mask_in_place为111,和它进行与就可以取出markOop中后三位,即(是否偏向锁+锁标志位)
      andptr(tmp_reg, markOopDesc::biased_lock_mask_in_place);
      // 将上面结果,即(是否偏向锁+锁标志位)和biased_lock_pattern再次比较(biased_lock_pattern为5,即101),如果不相等,则表明不为偏向锁状态,需要进行CAS操作,跳往cas_label;否则即为偏向锁状态,接着往下走。
      cmpptr(tmp_reg, markOopDesc::biased_lock_pattern);
      jcc(Assembler::notEqual, cas_label);
    
    

    2:走到这,表明锁对象已经为偏向锁态,需要判断锁对象之前是否已经偏向当前线程。

      // 将锁对象所属类的prototype_header移动至tmp_reg中,prototype_header中存储的也是markOop。
      // prototype_header是专门为偏向锁打造的,初始时类的prototype_header为偏向锁态,即后三位为101,一旦发生了bulk_revoke,那么就会设为无锁态,即001。
      // bulk_revoke为批量撤销,每次类发生bulk_rebais时(类的所有对象重设偏向锁),类prototype_header中的epoch就会+1,当epoch达到一个阈值时,就会发生bulk_revoke,撤销该类每个对象的偏向锁,这样该类的所有对象以后都不能使用偏向锁了,其实也就是虚拟机认为该对象不适合偏向锁。
      load_prototype_header(tmp_reg, obj_reg);
    
      // 将当前线程id和类的prototype_header相或,这样得到的markOop为(当前线程id + prototype_header中的(epoch + 分代年龄 + 偏向锁标志 + 锁标志位)),注意prototype_header的分代年龄那4个字节为0
      orptr(tmp_reg, r15_thread);
      // 将上面计算得到的结果与锁对象的markOop进行异或,tmp_reg中相等的位全部被置为0,只剩下不相等的位。
      xorptr(tmp_reg, swap_reg);
      Register header_reg = tmp_reg;
      // 对((int) markOopDesc::age_mask_in_place)进行按位取反,age_mask_in_place为...0001111000,取反后,变成了...1110000111,除了分代年龄那4位,其他位全为1;
      // 将取反后的结果再与header_reg相与,这样就把header_reg中除了分代年龄之外的其他位取了出来,即将上面异或得到的结果中分代年龄给忽略掉。
      andptr(header_reg, ~((int) markOopDesc::age_mask_in_place));
      // 如果除了分代年龄,对象的markOop和(当前线程id+其他位)相等,那么上面与操作的结果应该为0,表明对象之前已经偏向当前线程,即markOop中存放有当前线程id,那么跳到done处,直接进入方法执行即可;否则表明当前线程还不是偏向锁的持有者,会接着往下走。
      jcc(Assembler::equal, done);
    
    

    3:走到这,表明锁对象并没有偏向当前线程,接下来判断是否需要撤销锁对象的偏向。

      // 将header_reg和111相与,如果结果不为0,则表明header_reg后三位存在不为0的位,证明之前进行异或时,类的prototype_header后面三位与对象markOop的后三位不相等,但是能走到这,表明对象markword后三位为101,即偏向模式。现在类的prototype_header和对象markOop后三位不相等,即对象所属类不再支持偏向,发生了bulk_revoke,所以需要对当前对象进行偏向锁的撤销;否则表明目前该类还支持偏向锁,接着往下走。
      testptr(header_reg, markOopDesc::biased_lock_mask_in_place);
      jccb(Assembler::notZero, try_revoke_bias);
    
    

    4:走到这,表明锁对象还支持偏向锁,需要判断当前对象的epoch是否合法,如果不合法,需要取进行重偏向。合法的话接着往下走。

      // 测试对象所属类的prototype_header中epoch是否为0,不为0的话则表明之前异或时,类的prototype_header中epoch和对象markOop的epoch不相等,表明类在对象分配后发生过bulk_rebais()(前面提到过,每次发生bulk_rebaise,类的prototype header中的epoch都会+1),所以之前对象的偏向就无效了,需要进行重偏向。否则接着往下走。
      testptr(header_reg, markOopDesc::epoch_mask_in_place);
      jccb(Assembler::notZero, try_rebias);
    
    

    5:走到这,表明锁对象的偏向态合法,可以尝试去获取锁,使对象偏向当前线程。

      // 取出对象markOop中除线程id之外的其他位
      andptr(swap_reg,
             markOopDesc::biased_lock_mask_in_place | markOopDesc::age_mask_in_place | markOopDesc::epoch_mask_in_place);
      // 将其他位移动至 tmp_reg。
      movptr(tmp_reg, swap_reg);
      // 将其他位和当前线程id进行或,构造成一个新的完整的32位markOop,存入tmp_reg中。新的markOop因为保存了当前线程id,所以会偏向当前线程。
      orptr(tmp_reg, r15_thread);
      // 尝试利用CAS操作将新构成的markOop存入对象头的mark_addr处,如果设置成功,则获取偏向锁成功。
      // 这里说明下,cmpxchgptr操作会强制将rax寄存器(swap_reg)中内容作为老数据,与第二个参数,在这里即mark_addr处的内容进行比较,如果相等,则将第一个参数的内容,即tmp_reg中的新数据,存入mark_addr。
      cmpxchgptr(tmp_reg, mark_addr); // compare tmp_reg and swap_reg
      // 上面CAS操作失败的情况下,表明对象头中的markOop数据已经被篡改,即有其他线程已经获取到偏向锁,因为偏向锁不容许多个线程访问同一个锁对象,所以需要跳到slow_case处,去撤销该对象的偏向锁,并进行锁升级。
      if (slow_case != NULL) {
        jcc(Assembler::notZero, *slow_case);
      }
      // 上面CAS成功的情况下,直接就跳往done处,回去执行方法的字节码了。
      jmp(done);
    
    

    6:其实到这里,biased_locking_enter()已经结束了,不过上面多处提到了try_rebais和try_revoke,这两个其实就是汇编里的标号,它们对应的代码也定义在biased_locking_enter中。

      bind(try_rebias);
      // 将锁对象所属类的prototype_header送入tmp_reg。
      load_prototype_header(tmp_reg, obj_reg);
      // 尝试用CAS操作,使对象的markOop重置为无线程id的偏向锁态,即不偏向任何线程。
      cmpxchgptr(tmp_reg, mark_addr); 
      // 和第5步一样,如果CAS失败,则表明对象头的markOop数据已经被其他线程更改,需要跳往slow_case进行撤销偏向锁,否则跳往done处,执行字节码。
      if (slow_case != NULL) {
        jcc(Assembler::notZero, *slow_case);
      }
      jmp(done);
    
    
      bind(try_revoke_bias);
      // 走到这,表明这个类的prototype_header中已经没有偏向锁的位了,即这个类的所有对象都不再支持偏向锁了,但是当前对象仍为偏向锁状态,所以我们需要重置下当前对象的markOop为无锁态。
      // 将锁对象所属类的prototype_header送入tmp_reg。
      load_prototype_header(tmp_reg, obj_reg);
      // 尝试用CAS操作,使对象的markOop重置为无锁态。这里是否失败无所谓,即使失败了,也表明其他线程已经移除了对象的偏向锁标志。
      cmpxchgptr(tmp_reg, mark_addr); 
      //接下来会回到lock_object()方法中,继续轻量级锁的获取。
    
    

    五:总结

    上面根据同步方法讲了一下偏向锁,笔者在这上面也啃了差不多整个周六,原理看似很简单,但是在很多细节不清楚的情况下去看源码,尤其是这种全是汇编代码时,往往是一脸懵逼。而且HotSpot用一个并不是对象的markOop去表示锁,涉及到计算时更让人糊涂。如果大家只是想稍微了解下原理,建议还是不要太深入源码细节。。。。

    相关文章

      网友评论

      • aa0313280e7c:您好!有个疑问?bulk_rebais是什么时候发生的?是每一个有线程重偏向当前锁对象时就会导致批处理重偏向吗

      本文标题:JVM同步方法之偏向锁

      本文链接:https://www.haomeiwen.com/subject/sfgsqftx.html