锁的状态(偏向锁,轻量级锁及重量级锁)

本文详细介绍了Java中锁的三种状态:偏向锁、轻量级锁和重量级锁。偏向锁适用于线程独占的情况,轻量级锁通过自旋避免线程阻塞,而重量级锁依赖于操作系统MutexLock,适用于多线程竞争激烈的情况。锁的状态会随着竞争升级,从无锁->偏向锁->轻量级锁->重量级锁,但不会降级。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

本篇博客参考了死磕Synchronized底层实现--概论如果有兴趣了解更深的内容可以看看上面博客。

锁的状态

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。锁的状态保存在对象头的Mark Word中,以32位的JDK为例:

 

(一)偏向锁

   在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用它的线程拥有,这个线程就是锁的偏向线程。那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁。但只要有其他线程尝试获取锁,那么偏向锁就会升级为轻量级锁

偏向锁获取的过程

     1. 在当前线程的中创建一锁记录(Lock Record的空间,并将锁记录的obj属性指向锁对象。

     2. 判断Mark Work偏向锁的标记是否设置为1--确认为可偏向状态。

     3. 如果锁对象是第一次被线程获取,JVM会将对象头的锁标志设置为“01

     4. 然后使用CASMark Word 中的线程ID设置为当前线程ID,如果设置成功,那么持有偏向锁的线程以后每次进入该同步代码块,就不用进行任何操作

偏向锁的释放

   偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态(即当前线程是否在执行同步代码块),根据对象的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)轻量级锁(标志位为“00”)的状态。

(二)轻量级锁

  “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在多个线程同时尝试获取锁(CAS获取锁失败)的情况,就会导致轻量级锁膨胀为重量级锁

JVM8

CASE(_monitorenter): {
  oop lockee = STACK_OBJECT(-1);
  ...
  if (entry != NULL) {
   ...
   // 上面省略的代码中如果CAS操作失败也会调用到InterpreterRuntime::monitorenter

    // traditional lightweight locking
    if (!success) {
      // 构建一个无锁状态的Displaced Mark Word
      markOop displaced = lockee->mark()->set_unlocked();
      // 设置到Lock Record中去
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      //使用CAS将displaced替换到mark word中
      //cmpxchg_ptr方法第一个参数是预期修改后的值,第2个参数是修改的对象,
      //第3个参数是预期原值,方法返回实际原值,如果等于预期原值则说明修改成功。
      //如果锁对象的mark word被替换为锁记录,则其他线程就无法CAS成功
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
        // Is it simple recursive case?
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          // 有线程竞争锁则调用monitorenter
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

轻量级锁的获取过程

      1. 在进入同步代码块的时候,如果同步对象锁状态为无锁状态(锁标志为01,偏向锁标志为0JVM会在当前线程的中创建一个锁记录Lock Record)的空间(锁记录只作用于轻量级锁中,在偏向锁和重量级锁中只是起过渡作用),用于存储锁对象的Mark Word的拷贝---Displaced Mark Word如果是处于偏向锁状态,则使用原来的锁记录

      2. 拷贝之后,当前线程使用CAS锁对象的Mark Word更新为指向锁记录的指针,并将锁记录里的owner指针指向锁对象的mark word

      3. 如果成功更新,则表示当前线程成功获得了锁,并且对象Mark Word锁标志为“00,表示此对象处于轻量级锁状态。

      4. 如果更新失败JVM会检查对象的Mark Word是否指向当前线程的锁记录,如果是,则说明当前线程是锁重入,那么使用CAS把上面锁记录中的Displaced Mark Word设置为null,继续执行同步代码块。否则,说明有多个线程同时竞争锁,那么轻量级锁会膨胀为重量级锁。锁标志的状态值变为10后面等待锁的线程要进入阻塞状态 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

锁重入中的栈与对象头如下:

轻量级锁的释放过程:

    线程通过CAS将栈帧中的Displaced Mark Word 替换回锁对象的Mark Word中,如果替换成功,则释放锁。否则,说明有其他线程尝试获取锁(此时锁已经膨胀为重量级锁),那就要在释放锁的同时,唤醒被挂起的线程

轻量级锁主要有两种

1. 自旋锁

   所谓自旋,是指当有另一个线程想获取被其它线程持有的锁的时候,不会进入阻塞状态,而是使用空循环来进行自旋。注意:自旋是会消耗cpu所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。

自旋锁的一些问题

  如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在空循环,消耗cpu

   本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

   基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。

默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。

2.自适应自旋锁

    所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。

    延长自旋次数:假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。

    减少自旋次数:,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。

(三)重量级锁

   重量级锁是依赖对象内部的monitor来实现的,而monitor依赖操作系统MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。

   每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行同步代码块时,该线程得先获取到锁对象对应的monitor。当一个monitor被持有后,它将处于锁定状态。

JVM8:锁膨胀为重量级锁

for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;
      // CASE: inflated
      if (mark->has_monitor()) {
          // 已经是重量级锁状态了,直接返回
          ObjectMonitor * inf = mark->monitor() ;
          ...
          return inf ;
      }
      // CASE: inflation in progress
      if (mark == markOopDesc::INFLATING()) {
         // 正在膨胀中,说明另一个线程正在进行锁膨胀,continue重试
         TEVENT (Inflate: spin while INFLATING) ;
         // 在该方法中会进行spin/yield/park等操作完成自旋动作 
         ReadStableMark(object) ;
         continue ;
      }
 
      if (mark->has_locker()) {
          // 当前轻量级锁状态,先分配一个ObjectMonitor对象,并初始化值
          ObjectMonitor * m = omAlloc (Self) ;
          
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
          // 将锁对象的mark word设置为INFLATING (0)状态 
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }

          // 栈中的displaced mark word
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;

          // 设置monitor的字段
          m->set_header(dmw) ;
          // owner为Lock Record
          m->set_owner(mark->locker());
          m->set_object(object);
          ...
          // 将锁对象头设置为重量级锁状态
          object->release_set_mark(markOopDesc::encode(m));

         ...
          return m ;
      }
      // CASE: neutral
      // 分配以及初始化ObjectMonitor对象
      ObjectMonitor * m = omAlloc (Self) ;
      // prepare m for installation - set monitor to initial state
      m->Recycle();
      m->set_header(mark);
      // owner为NULL
      m->set_owner(NULL);
      m->set_object(object);
      ...
      // 用CAS替换对象头的mark word为重量级锁状态
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          // 不成功说明有另外一个线程在执行inflate,释放monitor对象
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
      }
      ...
      return m ;
  }

重量级锁的获取过程:

    在进入同步代码块的时候,如果对象锁处于轻量级锁(锁标志位为00),JVM会分配该对象一个monitor对象,并初始化。使用CAS将锁对象的Mark Word设置为INFLATING(膨胀中)状态,一旦设置成功,其他线程会自旋地等待膨胀完成。之后会将monitor对象的header属性设置为栈帧中的Displaced Mark Wordowner指针指向锁记录Lock Record),obj属性指向锁对象。锁标志的状态值设置为10mark word指向monitor对象。在将锁膨胀后,该线程会获取锁,并将monitor对象中的owner指针指向当前线程

   如果对象锁处于无锁状态(锁标志位为01),则不需要使用锁记录(Lock Record只用于轻量级锁中),会将monitor对象的header属性设置为对象锁的Mark Wordowner指针为当前线程obj属性指向锁对象,锁标志的状态值设置为10”,mark word指向monitor对象。

   如果获取锁失败,则将该线程放入到lock pool中,然后挂起该线程(进入阻塞状态),直到锁被释放时,才有可能抢夺锁成功,进入就绪状态。

   如果线程获得锁后调用wait方法,则会将线程加入到等待队列中,当被notify唤醒后,会将线程从等待队列移动到锁池中去。需要注意的是,当调用一个锁对象的waitnotify方法时,如当前锁的状态是偏向锁轻量级锁则会先膨胀成重量级锁

重量级锁的释放过程:

   设置monitor对象的owner指针为null,即释放锁,并唤醒被阻塞的线程。

为什么说重量级锁开销大呢

    主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值