深入理解java的Synchronized底层原理-01重量级锁

156 阅读8分钟

概述

synchronized是java中的一个关键字,用于多线程中。 他的实现依赖于JVM的Monitor(监视器锁)以及对象头(Object header)

当synchronized关键字修饰在方法或代码块上,会对对象进行加锁,从而保证同一时刻只有一个线程可以执行加锁了的代码块

但是修饰方法和修饰代码块,底层的逻辑并不一样

当synchronized修饰方法时: 方法的常量池会增加一个ACC_SYNCORONIZED标识,当某个线程访问到这个方法会检测是否有这个标志,如果有则需要获得监视器锁才可以执行方法。

当synchronized修饰代码块时: 会在代码块前后插入monitorenter和monitoexit字节码指令。加锁时,会执行monitorenter指令,而程序解锁时,会执行monitorexit解锁

那么这里就引入了两个关键信息: Object header和Monitor

Object Header

主要包含两个部分:MarkWord和KlassPointer 这里我们主要关注一下MarkWord,他是实现Synchronized的关键:

MarkWord用于保存对象运行时数据,比如锁状态,哈希码等信息

具体有这四种锁状态:

  • 未锁定状态:MarkWord存储对象哈希码和GC分代信息
  • 偏向锁状态:MarkWord保存获取该锁的线程ID和一些偏向锁标志位
  • 轻量级锁状态:MarkWord存储指向栈中锁记录的指针
  • 重量级锁状态:MarkWord存储指向Monitor对象的指针

Monitor

monitor是在HotSpot中用c++实现的,叫做ObjectMonitor 在java中,会调用monitorenter指令和monitorexit指令对线程进行加锁和解锁。

这里可以看一下下面代码编译后得到的字节码文件,可以看到两条指令的位置,一目了然

image.png

image.png

可以很明显的看到,当程序进入helloword方法准备输出helloword字符串时,会执行monitorenter命令,当执行该命令时,程序会进行争抢锁,只有抢到了,才可以进入到代码块中执行代码。而当输出完毕方法结束后,又会执行monitorexit指令解锁

但是图中可以看到有两条monitorexit指令,这是因为,有异常的情况也要进行解锁,否则就死锁了 这就是为什么,synchornized不需要手动解锁的原因,底层都帮我们做好了

接着我们继续深入synchornized synchornized是一个重量级锁,但是从jdk1.6以后,synchornized引入了一些锁状态,来提高其性能,包括偏向锁,轻量级锁,重量级锁。这些会根据线程的状态进行动态的升级和降级

synchonized重量级锁的实现:

上文中,我们提到,monitor底层是使用c++实现的,叫做ObjectMonitor。

image.png

上图中标注备注的几个属性字段是重要属性,源码和这几个字段关联很大

当java执行monitor指令的时候,jvm就会去调用monitor底层的代码:

加锁monitorenter

第一次CAS尝试获取锁

//可以看到,这里就是执行monitorenter指令执行的方法
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) {
   //fast_enter是偏向锁,这里我们先不讲
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
  //slow_enter是重量锁,看到这个名字就知道这玩意肯定慢,至少肯定比上面的fast慢
    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

最终,程序会进入到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 ;   //_recursions标记为1
    _owner = Self ;     //设置owner
    OwnerIsThread = 1 ;
    return ;
  }

注释里已经给出了重要代码的注解,大体逻辑就是,使用CAS把owner设置为当前线程,如果成功了那么就说明获取锁成功,然后设置recursions变量为1

CAS失败

当CAS操作失败,则会执行下面的代码:

    for (;;) {
      EnterI (THREAD) ;

  }
  
void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
	//如果CAS失败,在尝试CAS操作获取一次锁
    if (TryLock (Self) > 0) {  
        return ;
    }
 
    DeferredInitialize () ;
    //自适应自旋
    if (TrySpin (Self) > 0) {
        return ;
    }
    //到此,自旋终于全失败了,实在没办法了,要入队挂起了
    ObjectWaiter node(Self) ; //将Thread封装成ObjectWaiter对象
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ; 
    node.TState  = ObjectWaiter::TS_CXQ ; 
    ObjectWaiter * nxt ;
    for (;;) { //因为可能会当多个线程争抢所以这里循环,使用CAS操作
        node._next = nxt = _cxq ;//将node插入到_cxq队列的头部
        //CAS修改_cxq指向node
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
        if (TryLock (Self) > 0) {//还是不死心,还想挣扎一下看看能不能获取锁
            return ;
        }
    }

阻塞

如果这个过程,这么努力了还是没有抢到锁,那么就要进行阻塞了,下面是阻塞的代码:

//阻塞
    for (;;) {
        if (TryLock (Self) > 0) break ;//临死阻塞之前,我再挣扎一下
 
        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ; //彻底死心,挂起,阻塞。
        }
 
        if (TryLock(Self) > 0) break ;//当唤醒后,立马继续争抢锁
        //............

以上,就是Synchronized加锁的过程,过程非常曲折

总结

首先,当线程执行monitorenter指令后,会根据锁的类型,进入对应方法,重量级锁会进行slow_enter方法,最终执行ObjectMonitor::enter方法

该方法通过CAS操作设置当前线程,如果获取成功就标识获取锁成功,如果是第一次获取成功,那么recursions设置为1

如果不是第一次是冲入,那么就会递增recursions表示重入

如果CAS失败那么会循环,在尝试一下获取锁,如果还是失败,那么就自适应自旋,如果还失败,那么就要用到_cxq单向链表,把ObjectWaiter对象插入到里面,插入后,还会在进行CAS尝试一次,如果这次还是不行,那么只能进行阻塞了,但是在阻塞之前还要挣扎一次尝试获取锁。

可以看到,加锁的过程看似简单,但是过程非常曲折,尤其是第一次CAS失败,就会开始尝试多次CAS,自旋来尝试获取锁,总之就是能不阻塞就不阻塞,走投无路才阻塞

解锁monitorexit

解锁比起加锁来说就简单了,基本就是recursions的值来判断是否解锁,

if (_recursions != 0) {
_recursions--; // 如果_recursions次数不为0.自减,当减到0那么就解锁
TEVENT (Inflated exit - recursive) ;
return ;
}

至于wait,和notify,就是操作waitSet和队列

wait就是将当前线程加入到waitSet双向列表中,然后在执行exit来解锁

notify就是冲waitSet头部拿节点,然后根policy来判断是放到entrylist还是cxq中,并且进行唤醒

至于notifyAll就是循环然后全部唤醒

自旋

自旋其实就是空转CPU执行一些无意义的指令,目的是不让出CPU等待锁的释放。

自旋用来优化synchronized。因为synchronized底层会用到系统调用,会有很大的开销。

所以在线程竞争不激烈的时,可以进行自旋,说不定还没阻塞就获取到锁了,这样就避免了从进入阻塞状态然后再获取锁这个过程的一些没必要的开销,提高了性能

但是自旋也不是全是好处,因为如果线程竞争非常激烈的时,自旋就是在浪费cpu因为有很多线程在争抢自旋后肯定还是要阻塞。

所以java引入的是自适应自旋,动态进行自旋,至于自选的次数是根据上次自旋的次数来定的。

至此,Synchronized重量级锁的底层操作已经讲解完毕。

总结

synchronized底层使用monitor对象,cas操作进行的 monitor对象有几个重要的对象分别是:recursions锁的次数,owner拥有锁的线程,waitSet条件等待队列和cxq,EntryList等待队列

当线程没有抢到锁,会进行自旋,如果还是没抢到那么阻塞。

解锁是通过recursions来判断是否解锁,当resucrsions为0那么就解锁,如果不为0那么递减

至于wait和notify,notifyAll都是操作waitSet和cxq,Entry队列进行入队和出队操作。

由于synchronized底层依赖于操作系统实现,系统调用存在用户态和内核态之间的切换,所以开销很大,称之为重量级锁所以引入了自适应自旋机制,来提高锁的性能

但是synchronized的优化不仅仅只有自旋一种方法,1.6还引入了偏向锁,轻量锁,锁消除,锁粗化的概念,后面的文章会继续讲解。