Java Synchronized 重量级锁原理深入剖析上(互斥篇)

1,182 阅读14分钟

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
上篇文章分析了偏向锁、轻量级锁的演变过程,本篇将分析重头戏:重量级锁的原理。
通过本篇文章,你将了解到: 1、ObjectMonitor 的运用
2、锁的膨胀过程
3、重量级锁的加锁流程
4、重量级锁的解锁流程
5、重量级锁小结
6、与偏向锁、轻量级锁的比对

1、ObjectMonitor 的运用

我们知道当锁处在轻量级锁的状态时,Mark Word 存放着指向Lock Record指针,Lock Record是线程私有的。
而处在重量级锁状态时说明有线程没拿到锁需要阻塞等待锁,当拥有锁的线程释放锁后唤醒它继续竞争锁。此处就引入了一个问题:其它线程如何找到被阻塞的线程?我们很容易想到:把阻塞的线程放到多线程共享的(能访问)的列表里。
而Lock Record是线程私有的,显然不能满足需求。
因此,重量级锁引入了ObjectMonitor类。

image.png

如上图,Mark Word 存放着指向ObjectMonitor的指针,ObjectMonitor是线程间共享的并且拥有比Lock Record更多的信息。
来看看ObjectMonitor 记录的信息:

#ObjectMonitor.hpp
  ObjectMonitor() {
    //记录无锁状态的Mark Word
    _header       = NULL;
    _count        = 0;
    //等待锁的线程个数
    _waiters      = 0,
    //线程重入次数
    _recursions   = 0;
    //指向的对象头
    _object       = NULL;
    //锁的本身,指向线程或者Lock Record
    _owner        = NULL;
    //调用wait()方法后等待锁的队列
    _WaitSet      = NULL;
    //等待队列的锁
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    //ObjectWaiter 队列
    _cxq          = NULL ;
    FreeNext      = NULL ;
    //ObjectWaiter 队列
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

可以看出,Lock Record里拥有的信息ObjectMonitor里也有,如存储Mark Word的_header字段,存储指向对象头的指针_object字段。当然,ObjectMonitor还有更丰富的信息,如获取锁失败存放阻塞线程的队列_cxq,调用wait()方法后等待的线程队列_WaitSet等。

2、锁的膨胀过程

知道有ObjectMonitor这个东西了,接下来看看如何使用它。
回顾之前的分析,偏向锁升级为轻量级锁时要修改Mark Word,使之指向Lock Record,轻量级锁升级为重量级锁时也需要修改Mark Word,使之指向ObjectMonitor。
而创建/获取ObjectMonitor 对象的过程即是锁的膨胀过程。
源码里的膨胀过程就是个inflate(xx)函数:

#synchronizer.cpp
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  ...
  //死循环,直到获取到ObjectMonitor为止
  for (;;) {
      //取出Mark Word
      const markOop mark = object->mark() ;
      //如果是重量级锁
      if (mark->has_monitor()) {
          //是重量级锁,说明肯定已经有现成的ObjectMonitor,直接用就好了
          ObjectMonitor * inf = mark->monitor() ;
          return inf ;
      }

      //正在膨胀的时候
      if (mark == markOopDesc::INFLATING()) {
        //继续循环,需要等待膨胀完成
         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更改为膨胀状态,此时Mark Word 全是0 --------->(1)
          //可能会有多线程走到这,因此用CAS
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
          if (cmp != mark) {
             //修改失败,继续循环
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }

          //若是修改成功,则取出之前轻量级锁存储的Mark Word
          markOop dmw = mark->displaced_mark_helper() ;
          //将Mark Word 搬到ObjectMonitor的_header字段里
          m->set_header(dmw) ;

          //_owner指向Lock Record,也就是设置锁的持有者是Lock Record------->(2)
          m->set_owner(mark->locker());
          //指向对象头
          m->set_object(object);
          //将Mark Word 指向ObjectMonitor------->(3)
          object->release_set_mark(markOopDesc::encode(m));
          ...
          //成功,则返回ObjectMonitor 对象
          return m ;
      }

      //无锁状态
      ObjectMonitor * m = omAlloc (Self) ;
      //初始化一些参数
      m->Recycle();
      //直接记录mark
      m->set_header(mark);
      //_owner为空-------------------->(4)
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class

      //将Mark Word修改为指向ObjectMonitor的指针-------------------->(5)
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          ...
          //失败,则重新尝试
          continue ;
      }
      ...
      //成功,则返回ObjectMonitor 对象
      return m ;
  }
}

上述代码即为简化过的膨胀流程,标注了5个重点:
(1)
如果当前锁是轻量级锁,说明有线程正在持有该锁,尝试CAS修改锁为膨胀状态。
(2)
_owner不指向任何线程,指向的是Lock Reocrd,后续会有相应的判断。
(3)
轻量级锁时Mark Word存储着指向Lock Record的指针,而此时变为指向重量级锁的指针,也就是指向 ObjectMonitor的指针。此处是单线程操作,因此可以直接设置。
markOopDesc::encode(m) 定义如下:

#markOop.hpp
  static markOop encode(ObjectMonitor* monitor) {
    intptr_t tmp = (intptr_t) monitor;
    //Mark Word指向ObjectMonitor
    return (markOop) (tmp | monitor_value);
  }

(4)
如果当前锁是无锁状态,将_owner置空。
(5)
CAS尝试将Mark Word 指向ObjectMonitor。

以上就是膨胀的流程,用图表示如下:

image.png

3、重量级锁的加锁流程

初次尝试加锁

回顾偏向锁、轻量级锁加锁流程核心:修改Mark Word。
而在膨胀为重量级锁时也是修改了Mark Word,不同的是此过程并没有线程占用重量级锁。来看看重量级锁的抢占过程:

#ObjectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
  //当前线程
  Thread * const Self = THREAD ;
  void * cur ;

  //尝试修改_owner字段为当前线程,也就是尝试获取锁
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
    //修改成功,则获取了重量级锁
     return ;
  }

  //以下都是CAS失败后的处理
  //如果当前_owner值为当前线程,则认为是重入了该锁
  if (cur == Self) {
    //重入次数+1,成功获取了锁
     _recursions ++ ;
     return ;
  }

  //_owner值为Lock Record,说明当前线程是之前轻量级锁的持有者
  if (Self->is_lock_owned ((address)cur)) {
    //重入次数为1次
    _recursions = 1 ;
    //改为当前线程
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
  ...

  { 
    ...
    for (;;) {
      //没有获取到锁,则执行该函数
      EnterI (THREAD) ;
      ...
      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;
      jt->java_suspend_self();
    }
  }

由上可知,enter(xx)函数主要做了如下事情:

先CAS尝试修改ObjectMonitor的_owner字段,会有几种结果:
1、锁没被其它线程占用,当前线程成功获取锁。
2、锁被当前线程占用,当前线程重入该锁,获取锁成功。
3、锁被LockRecord占用,而LockRecord又属于当前线程,属于重入,重入次数为1。
4、以上条件都不满足,调用EnterI()函数。

用图表示如下:

image.png

再次尝试加锁

初次获取锁失败后,会走到下面的流程,也就是EnterI()函数的实现:

#ObjectMonitor.cpp
void ATTR ObjectMonitor::EnterI (TRAPS) {
    //当前线程
    Thread * Self = THREAD ;
    //尝试加锁----------->(1)
    if (TryLock (Self) > 0) {
        return ;
    }
    //尝试自旋加锁----------->(2)
    if (TrySpin (Self) > 0) {
        return ;
    }
    //构造ObjectWaiter 节点
    ObjectWaiter node(Self) ;
    //挂起/唤醒线程重置参数
    Self->_ParkEvent->reset() ;
    //前驱节点为无效节点
    node._prev   = (ObjectWaiter *) 0xBAD ;
    //当前节点状态为CXQ,也就是说节点在_cxq队列里
    node.TState  = ObjectWaiter::TS_CXQ ;

    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        //将节点插入_cxq队列的头----------->(3)
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

        //尝试获取锁----------->(4)
        if (TryLock (Self) > 0) {
            return ;
        }
    }
    ...
    for (;;) {
        //再次尝试获取锁----------->(5)
        if (TryLock (Self) > 0) break ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }
        //挂起线程----------->(6)
        if (_Responsible == Self || (SyncFlags & 1)) {
            //挂起有超时时间
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
        } else {
            //挂起没有超时时间
            Self->_ParkEvent->park() ;
        }
        //唤醒后再次获取锁,成功则退出循环----------->(7)
        if (TryLock(Self) > 0) break ;
        //...还是一些自旋策略
    }
    //将节点从_cxq或_EntryList里移除----------->(8)
    UnlinkAfterAcquire (Self, &node) ;
    ...
    return ;
}

上述代码标注了8点重点,来看看更详细的解释:
(1)
TryLock 顾名思义尝试获取锁:

#ObjectMonitor.cpp
int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      //for 循环名存实亡
      void * own = _owner ;
      //中途判断_owner是否已经被更改,若是则退出
      if (own != NULL) return 0 ;
      //还是尝试更新_owner
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
         return 1 ;
      }
      if (true) return -1 ;
   }
}

(2)
TryLock 只执行一次CAS,而TrySpin顾名思义:自旋获取锁。

#ObjectMonitor.cpp
int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
   ...
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {
         ...
        return 1 ;
      }
      //休息一下继续
      SpinPause () ;
    }
    ...
}

可以看出TrySpin里多次调用TryLock,次数是10次。源码里指出经验值20-100可能最佳。
(3)
此处是死循环,直到插入队列成功或者获取了锁。
此处是往_cxq写数据,并且它的_next指针指向_cxq,因此每次新节点都放在队列头。又因为可能存在多线程修改_cxq,因此需要CAS。
(4)
插入队列失败后,再尝试获取锁。
(5)
又是个死循环,先尝试获取锁。
(6)
至此,线程放弃获取锁的动作,将自己挂起了,线程阻塞于此处,等待别的线程唤醒它。
(7)
当某个线程唤醒在(6)被挂起的线程后,被唤醒的线程立即再尝试获取锁,如果还是失败了,则继续回到(5)的循环。
(8)
获取锁成功后,因为前边已经加入到队列了,因此需要将节点从队列(_cxq/_EntryList)移除。

通过上述(1)~(8)的分析可知,enterI()函数主要做了如下事情:

1、多次尝试加锁。
2、实在不行将线程包装后加入到阻塞队列里。
3、再尝试获取锁。
4、失败后将自己挂起。
5、被唤醒后继续尝试获取锁。
6、成功则退出流程,失败继续走上面的流程。

用图表示如下:

image.png

4、重量级锁的解锁流程

上面分析了加锁的过程,它有两种结果:

1、成功获取锁,那么可以执行临界区代码。
2、获取锁失败,挂起等待别人唤醒。

关于2思考一个问题:是谁唤醒了它,如何唤醒的?
先来看看1,线程执行完临界区代码后需要释放锁,偏向锁和轻量级锁的释放上篇文章已经分析:若是释放失败,则会走到重量级锁的释放流程。
重量级锁的释放流程,也就是exit()函数的实现:

#ObjectMonitor.cpp
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
   //释放锁的线程不一定是重量级锁的获得者-------->(1)
   if (THREAD != _owner) {
     if (THREAD->is_lock_owned((address) _owner)) {
       //释放锁的线程是轻量级锁的获得者,先占用锁
       _owner = THREAD ;
     } else {
       //异常情况
       return;
     }
   }

   if (_recursions != 0) {
      //是重入锁,简单标记后退出
     _recursions--;
     return ;
   }
   ...

   for (;;) {
      if (Knob_ExitPolicy == 0) {
         //默认走这里
         //释放锁,别的线程可以抢占了
         OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
         OrderAccess::storeload() ;                         // See if we need to wake a successor
         //如果没有线程在_cxq/_EntryList等待,则直接退出
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            TEVENT (Inflated exit - simple egress) ;
            return ;
         }
         //有线程在等待,再把之前释放的锁拿回来
         if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
            //若是失败,说明被人抢占了,直接退出
            return ;
         }
      } else {
         ...
      }

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;
      //此处省略代码
      //根据QMode不同,选不同的策略,主要是操作_cxq和_EntryList的方式不同
      //默认QMode=0

      w = _EntryList  ;
      if (w != NULL) {
         //_EntryList不为空,则释放锁---------(2)
          ExitEpilog (Self, w) ;
          return ;
      }

      //_EntryList 为空,则看_cxq有没有数据
      w = _cxq ;
      if (w == NULL) continue ;//没有继续循环

      for (;;) {
          //将_cxq头节点置空
          ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
          if (u == w) break ;
          w = u ;
      }
      if (QMode == 1) {
         ...
      } else {
         // QMode == 0 or QMode == 2
         //_EntryList指向_cxq
         _EntryList = w ;
         ObjectWaiter * q = NULL ;
         ObjectWaiter * p ;
         //该循环的目的是为了将_EntryList里的节点前驱连接起来---------(3)
         for (p = w ; p != NULL ; p = p->_next) {
             guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
             //改为ENTER状态
             p->TState = ObjectWaiter::TS_ENTER ;
             p->_prev = q ;
             q = p ;
         }
      }

      w = _EntryList  ;
      if (w != NULL) {
          //释放锁---------(4)
          ExitEpilog (Self, w) ;
          return ;
      }
   }
}

依旧是列出了4个点,exit()函数主要做了如下事情:
(1)
若膨胀的时候锁是轻量级锁,此时_owner指向Lock Record。当轻量级锁的占有者线程释放锁后会走到此,因此释放锁的线程不一定是重量级锁的获得者。
(2)
ExitEpilog (Self, w) 释放锁:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
   //从队列节点里取出ParkEvent
   ParkEvent * Trigger = Wakee->_event ;
   Wakee  = NULL ;

   //释放锁,将_owner置空
   OrderAccess::release_store_ptr (&_owner, NULL) ;
   OrderAccess::fence() ;                               // ST _owner vs LD in unpark()
   //唤醒节点里封装的线程
   Trigger->unpark() ;
}

release_store_ptr内部是汇编语句实现的原子操作。
(3)
之前_EntryList只用了后驱节点,也就是单向链表实现的队列,此处将前驱节点使用上了,也就是_EntryList变为双向链表了。
(4)
和(2)一样的作用,释放锁并唤醒对应的线程。

再来看看上面提出的2问题,从释放锁的流程已经得知:

当前占有锁的线程释放锁后会唤醒阻塞等待锁的线程

具体唤醒哪个线程,要看QMode值,以默认值QMode=0为例:

1、若是_EntryList队列不为空,则取出_EntryList队头节点并唤醒。
2、若是_EntryList为空,将_EntryList指向_cxq,并取出队头节点唤醒。

用图表示如下:

image.png

5、重量级锁小结

从加锁、解锁的流程可以明显地看出:

1、加锁过程是不断地尝试加锁,实在不行了才放入队列里,而且还是插入队列头的位置,最后才挂起自己。
2、想象一种场景:现在A线程持有锁,B线程在队列里等待,在A释放锁的时候,C线程刚好插进来获取锁,还未等B被A唤醒,C就获取了锁,B苦苦等待那么久还是没有获取锁。B线程不排队的行为造成了不公平竞争锁。
3、再想象另一种场景:还是A线程持有锁,B线程在队列里等待,此时C线程也要获取锁,因此要进入队列里排队,此处进入的是队列头,也就是在B的前面排着。当A释放锁后,唤醒队列里的头节点,也就是C线程。C线程插队的行为造成了不公平竞争锁。
4、综合1、2、3点可知,因为有走后门(不排队)\、插队(插到队头)、重量级锁是不公平锁。

综合加锁、解锁流程,用图表示如下:

image.png

图上流程对应的场景如下:

1、线程A先抢占锁,A在进入阻塞队列前已经成功获取锁。
2、而后线程B抢占锁,发现锁已被占有,于是加入阻塞队列队头。
3、最后线程C也来抢占锁,发现锁已经被占有,于是加入阻塞队列队头,此时B已经被C抢了队头位置。
4、当A释放锁后,唤醒阻塞队列里的队头线程C,C开始去抢占锁。
5、C拿到锁后,将自己从阻塞队列里移出。
6、后面的流程和之前一样。

上面的流程可能比较枯燥,用代码来演示以上场景:

public class TestThread {
    static Object object = new Object();
    static Thread a, b, c;
    public static void main(String args[]) {

        a = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("A before get lock");
                synchronized (object) {
                    System.out.println("A get lock");

                    try {
                        Thread.sleep(1000);
                        b.start();

                        //等待b已经启动并去抢占锁
                        Thread.sleep(1000);
                        c.start();

                        //等待b/c都已经启动,并且去抢占锁
                        Thread.sleep(2000);
                    } catch (Exception e) {

                    }
                }
                System.out.println("A after get lock");
            }
        });
        a.start();

        b = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("B before get lock");
                synchronized (object) {
                    System.out.println("B get lock");
                }
                System.out.println("B after get lock");
            }
        });
        b.start();

        c = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("C before get lock");
                synchronized (object) {
                    System.out.println("C get lock");
                }
                System.out.println("C after get lock");
            }
        });
        c.start();
    }
}

每次输出结果都很固定:

image.png

可以看出与我们预期的一致:B虽然先去抢占锁,但总是被后来者的C先抢到锁,不公平之处尽显。

6、与偏向锁、轻量级锁的比对

至此,偏向锁、轻量级锁、重量级锁都已经分析完毕。
锁的核心在于谁是锁?

对于偏向锁和轻量级锁,"锁"是Mark Word。
对于重量级锁,"锁"是ObjectMonitor。

更多关于三者的异同以及适用场景请移步上篇文章:Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程

本篇文章分析了重量级锁的互斥过程,下篇文章将会分析与重量级锁紧密相关的同步过程(wait/notify/notifyAll)。

本文源码基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Java/Android