Java并发编程系列:Synchronized原理解析

从对象头的说起

https://blog.csdn.net/TheLudlows/article/details/75340361 文章中介绍了对象的内存布局。

如上图所示,Mark Word 存储对象自身的运行时数据如哈希值等,例如对象的hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。也就是说并不是对象在任何时刻标记为绿色的属性都会存在,在32位系统占4字节,在64位系统中占8字节。

Java中的每一个Object在JVM内部都会有一个native的C++对象oop/oopDesc与之对应。在hotspot源码 oop.hpp中oopDesc的定义如下

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

其中 markOop就是我们所说的Mark Word,用于存储锁的标识。

以32位虚拟机为例:

锁状态存储内容是否偏向锁标记
无锁对象的哈希码25bit + 分代年龄4bit001
偏向锁持有偏向锁的线程ID23bit + 时间戳(epoch)2bit + 分代年龄4bit101
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁(监视器)的指针10

锁的状态共有四种:无锁态、偏向锁、轻量级锁和重量级锁,在JDK1.6之前,Synchronized是一个重量级锁,性能比较差。之后为了减少获得锁和释放锁带来的性能消耗而引入偏向锁和轻量级锁。

四种锁的状态会随着竞争情况逐渐升级,锁可以升级但是不能降级,意味着偏向锁可以升级为轻量级锁但是轻量级锁不能降级为偏向锁,目的是为了提高获得锁和释放锁的效率。

字节码层面

下面从字节码的层面理解synchronized关键字,我们编写如下代码,并通过Javap -v 命令查看它的字节码:

public synchronized void lockMethod() {
    i++;
}

public void lockBlock() {
    synchronized (this) {
        i--;
    }
}

字节码如下:

public synchronized void lockMethod();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    //省略...
    
public void lockBlock();
    // 省略...
    3: monitorenter
    //....
    21: monitorexit

同步代码块反编译后,可以看到monitorenter和monitorexit两条指令。monitorenter处于代码块开始的位置,而monitorexit与之匹配在代码结束或者异常处。,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor并设置计数器值为0 ,其他线程将有机会获取 monitor 。

同步方法使用ACC_SYNCHRONIZED标记,表明进入此方法需要先获取到monitor,逻辑和同步块基本类似。

Synchronized的源码

从 monitorenter和 monitorexit这两个指令来开始阅读源码,JVM将字节码加载到内存以后,会对这两个指令进行解释执行,
monitorenter, monitorexit的指令解析是通过 InterpreterRuntime.cpp中的两个方法实现

InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)
InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  ...
  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);
  }
  ...
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

参数JavaThread thread指向java中的当前线程;BasicObjectLock类型的elem对象包含一个BasicLock类型_lock对象和一个指向Object对象的指针_obj,BasicLock中有mark word数据
UseBiasedLocking标识虚拟机是否开启偏向锁功能,JDK1.7之后默认开启偏相锁。

  • 如果开启偏向锁,则执行 ObjectSynchronizer::fast_enter的逻辑
  • 如果未开启偏向锁,则执行 ObjectSynchronizer::slow_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) ;
}

再次判断手否开启偏向锁,并且不处于安全点,PS:安全点指的是STW的时刻,所有线程被挂起。
接下来真正获取偏向锁的逻辑在revoke_and_rebias方法中。

偏向锁

在大多数的情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。
同步代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
对象头中存在指向监视器(monitor)的指针,当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下

ObjectMonitor() {
    _header       = NULL;//markOop对象头
    _count        = 0;
    _waiters      = 0,//等待线程数
    _recursions   = 0;//重入次数
    _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _owner        = NULL;//指向获得ObjectMonitor对象的线程或基础锁
    _WaitSet      = NULL;//处于wait状态的线程,会被加入到waitSet;
    _WaitSetLock  = 0;
    _Responsible  = NULL;
    _succ         = NULL;
    _cxq          = NULL;
    FreeNext      = NULL;
    _EntryList    = NULL;//处于等待锁block状态的线程,会被加入到entryList;
    _SpinFreq     = 0;
    _SpinClock    = 0;
    OwnerIsThread = 0;
    _previous_owner_tid = 0;//监视器前一个拥有者线程的ID
}

ObjectMonitor中有两个队列,WaitSet 和 EntryList,都用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),前者保存调用了wait方法的线程,后者保存等待锁的线程。
owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 EntryList 集合,当线程获取到对象的monitor
后进入 Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,
owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取monitor。
lock

获取锁

锁竞争发生在ObjectMonitor::enter方法中。

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  //通过CAS尝试把monitor的`_owner`字段设置为当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  //获取锁失败
  if (cur == NULL) {         assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }
  // 如果旧值和当前线程一样,说明当前线程已经持有锁,此次为重入,_recursions自增,并获得锁。
  if (cur == Self) { 
     _recursions ++ ;
     return ;
  }

  // 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
  if (Self->is_lock_owned ((address)cur)) { 
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
  // 省略部分代码。
  // 通过自旋执行ObjectMonitor::EnterI方法等待锁的释放
  for (;;) {
	  jt->set_suspend_equivalent();
	  EnterI (THREAD) ;
	  if (!ExitSuspendEquivalent(jt)) break ;
	      _recursions = 0 ;
	  _succ = NULL ;
	  exit (Self) ;
	  jt->java_suspend_self();
  }
}
  1. 通过CAS尝试把monitor的_owner字段设置为当前线程
  2. 如果_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行_recursions ++ ,记录重入的次数
  3. 如果是第一次进入该monitor,设置_recursions为1,_owner为当前线程,该线程成功获得锁并返回
  4. 通过自旋执行ObjectMonitor::EnterI方法等待锁的释放,在EnterI方法方法中,会把线程封装为ObjectWaiter对象,添加到EntryList并且通过CAS尝试一定次数获取锁,如果失败,则挂起线程。
释放锁
void ATTR ObjectMonitor::exit(TRAPS) {
   Thread * Self = THREAD ;
   //如果当前线程不是Monitor的所有者
   if (THREAD != _owner) { 
     if (THREAD->is_lock_owned((address) _owner)) { // 
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
    // 如果_recursions次数不为0.自减
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }
}

根据不同的策略,从EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。被唤醒的线程,继续执行monitor的竞争。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值