大聪明面试记:Synchronized

image.png

前言

大聪明面试记,一步一步成为Offer收割机

在Java面试中,只要涉及到多线程,基本上90%都会问synchronized,一些同学通过背面试题记住了部分知识点,但是对于原理以及底层却不明所以,本文将从用法逐步深入底层实现,让你一次性掌握synchronized的所有知识,成为Offer收割机。

正文

大聪明最近找工作,开场面试官就是一套并发三连:synchronized,volatile,AQS。打得大聪明措手不及,大聪明赶紧回家向自己哥哥大黄牛请教。

一.使用场景

为了解决多线程并发引起的一系列问题,Java为我们提供了多种并发工具,其中使用synchronized对资源进行加锁是我们最常用的一种。一般使用场景分为以下几种:

可以用来修饰方法(锁的当前实例对象),代码块(需要通过一个对象加锁,锁的括号里面的对象),静态方法(锁整个class对象)

1.修饰方法:此时锁住的是当前实例对象this。

public class SyncTest {

    public synchronized void test(){
        //do something
    }
    
}
复制代码

2.修饰代码块:此时锁住的是同步对象obj

public class SyncTest {
  
    public static  void test(){
            Object obj = new Object();
        synchronized (obj){
            //do something...
        }
    }  
    
}
复制代码

3.修饰静态方法,此时锁的是当前类的class加锁

public class SyncTest {

    public static synchronized void test(){
        //do something
    }
    
}
复制代码

二.实现原理

为了探究synchronized是怎么实现的,我写了一个测试代码,大家可以自己拷贝下来尝试通过下面的命令来反编译一下看结果。

– javap -c -p -v xxx.class

测试代码:

public class SyncTest {
    Test test = new Test();
    public synchronized void test1(){
        synchronized (new Integer(1)){
            System.out.println("test1");
        }
    }
}
复制代码

经过反编译我们可以看到如下结果,

1.在进入同步代码快时,会有 monitorenter 命令,此时会获取锁的所有权。

2.在退出同步代码块时,会有 monitorexit 命令,此时表示解锁操作。

3.在修饰的方法时,通过 ACC_SYNCHRONIZED 标志,此时会隐式的调用 monitorentermonitorexit

4.在第26行,还有一个 monitorexit ,是为了在异常的时候也能正常退出,有一个隐式的try-finally,这个在finally里面调用了 monitorexit

  public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  //方法锁的标记位,会隐式调用monitorenter和monitorexit
    Code:
      stack=3, locals=3, args_size=1
         0: new           #5                  
         3: dup
         4: iconst_1
         5: invokespecial #6                  
         8: dup
         9: astore_1
        10: monitorenter                //monitor标记进入
        11: getstatic     #7                 
        14: ldc           #8                  
        16: invokevirtual #9                  
        19: aload_1
        20: monitorexit               //monitor标记退出
        21: goto          29
        24: astore_2
        25: aload_1
        26: monitorexit               //还有一个退出是为了在异常的时候也能正常退出    隐式的try-finally,这个monitorexit在finally里面

复制代码

需要注意的两点:
**
1.修饰方法时(锁方法时):修饰方法的时候字节码中不会有 monitorentermonitorexit ,而是会在方法上标记 ACC_SYNCHRONIZED ,当JVM调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

2.修饰代码块时(锁对象时):在字节码中的 monitoreenter 代表获取锁的动作,monitorexit 代表释放锁。在编译器生成字节码的时候就将获取锁和释放锁的动作帮我们做好了。

所以,两种方法本质没有区别

三.锁升级过程和源码分析

1.知识储备

在进入锁升级和源码之前我们需要了解一些基本信息:java对象结构,monitor对象结构,JDK6的优化

Java对象结构

java对象存储在堆内存中,分为三个部分:对象头,实例数据,对其填充。

image.png

对象头由三部分组成:

1.markword: 用于存储对象自身的运行时数据,包括哈希码,分代年龄,锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。并且在不同锁的状态下,markword是不同的,不同级别的锁markword状态也是不同的。

markword不同锁状态下的结构:

a. 无锁:56bit存放hashcode,4bit存放分代年龄,1bit存放是否偏向值为0,2bit存放锁标志01

b. 偏向锁:54bit存放线程id,2bit存放epoch,4bit分代年龄,1bit存放是否偏向值为1,2bit锁标志位01

c. 轻量级锁:前面全部用来存放只想持有锁的线程的lockRecord的指针,锁标志位为00

d. 重量级锁:前面全部用来存放直向monitor的指针,锁标志为10

e. GC标记:内容为空,锁标志位 11

image.png

2.Klass指针: 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

3.数组长度: 非必须,当是数组时用来表示数组的长度。

实例数据: 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

对其填充: 对象必须是8字节整数倍,如果不是就需要补全对齐

monitor对象结构

大家还记得在测试代码反编译过后有 monitorentermonitorexit,那这个monitor究竟是什么呢?

在c++源码中我们可以找到对应的实现,当锁升级成为重量级锁的时候,对象头的信息会变成指向monitor的指针,这个monitor在HostSprt的实现是 ObjectMonitor 类,结构如下

ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;  
  _waiters      = 0,  
  _recursions   = 0;       //线程的重入次数
  _object       = NULL;   //存储锁的对象,sync(object[这个object])

  _owner        = NULL;    //标识拥有该锁的线程
  _WaitSet      = NULL;    //调用wait()方法的线程
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //抢锁失败的阻塞队列
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //另一个抢锁失败的阻塞队列,根据不同的模式,同 _cxq一起控制唤醒哪个线程
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;   //当前owner是否是线程指针
} 

在注释中有一段话:线程在任何时间组多出现在一个列表上,要么是cxp,要么是EntryList,要么在WaitSet
复制代码

聪明的你一定发现了,这个结构很类似AQS,所以理解了一个锁,对于另一个锁也能够很快了解。

JDK6的优化

在JDK6之前,synchronized是以重量级锁的方式加锁的。就会涉及到线程的阻塞和唤醒等操作,而这些操作是需要通过操作系统内核调用特权指令的。大家只要记住凡是涉及到特权指令调用,就会造成用户态和内核态的切换,这种切换是很耗费资源的,因为操作系统要保存线程运行状态,并且特权指令调用之后还要恢复线程运行状态,然后程序才能继续运行。而为了解决这个问题,JDK6之后引入了偏向锁和轻量级锁,在重量级锁中引入了自旋操作来避免线程阻塞,减少内核态和用户态切换。

2. 锁升级过程和加锁解锁过程

我们之前说过,不同锁的实现是对 mark word 的不同操作,那么我们来看看不同级别锁具体是怎么操作的吧。

偏向锁

image.png

偏向锁通过 mark word 的是否支持偏向锁标记(001)来进行判断,如果支持就会通过CAS操作将自己线程id记录到markword中,并且修改锁标志记101,之后如果还有线程获取锁就会判断锁标志是否101,线程是否是自己。
这部分实现在 synchronizer.cpp 中。

解锁操作 需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,则会升级为轻量级锁,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。

image.png
加锁源码

// -----------------------------------------------------------------------------
//  Fast Monitor Enter/Exit
// This the fast monitor enter. The interpreter and compiler use
// some assembly copies of this code. Make sure update those code
// if the following function is changed. The implementation is
// extremely sensitive to race condition. Be careful.

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) ;
}
复制代码
轻量级锁

image.png
在偏向锁遇到竞争或者关闭了偏向锁时,会进入轻量级锁的逻辑。轻量级锁的目的时避免系统调用,减少开销。
轻量级锁操作的就是对象头的 MarkWord 。

加锁: 如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw(就是那个set_displaced_header 方法执行的)里。
然后通过 CAS 把 mark word 指向这个 LockRecord 。
如果有锁则会看是不是自己的LockRecord,如果是就再放入一个null到LockRecord的header。

image.png
加锁源码

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) { //如果当前是无锁状态, 001
    //直接把mark保存到BasicLock对象的_displaced_header字段
    lock->set_displaced_header(mark);
    //通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ... 
  }
  //如果markword处于加锁状态并且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要争抢锁 
  else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif
  //代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过`inflate`进行膨胀升级为重量级锁
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

复制代码

解锁: 获取本地栈中的LockRecord的header,如果是null则弹出heade(说明重入了)r,如果不是null通过CAS将markword的内容写回到对象中,此时如果CAS失败,也会进入膨胀过程,因为可能其他线程已经将markword改成指向monitor了

解锁源码

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {//如果header为null,说明这是线程重入的栈帧,直接返回,不用回写
     mark = object->mark() ;
     assert (!mark->is_neutral(), "invariant") ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
     }
     return ;
  }
 
  mark = object->mark() ;
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     //CAS将Mark Word内容写回
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  //CAS操作失败,轻量级锁膨胀,因为其他线程竞争时造成了锁升级,需要走重量级锁的逻辑
   ObjectSynchronizer::inflate(THREAD, object)->exit (THREAD) ;
}
复制代码
重量级锁

进入重量级锁时会生成 ObjectMonitor 对象,然后 mark word 会指向这个对象。重量级锁通过 ObjectMonitor 里面的 ObjectMonitor::enterObjectMonitor::exit 来控制加锁解锁

ObjectMonitor::enter调用流程判断如果不是偏向锁则会走到重量级锁的过程,最终会执行到 ObjectMonitor::enter方法。会通过Atomic::cmpxchg_ptr(Self, &_owner, NULL);来设置_owner为当前线程。如果成功就设置重入次数,失败则会进入失败逻辑(ObjectMonitor::EnterI(TRAPS))。再次尝试CAS抢锁,如果还失败则会通过自旋(OBjectMonitor::TrySpin_VaryDuration),自旋时调用抢锁方法TryLock(Self)。默认自旋次数为10,如果还是失败则会调用EntryI()来进行入队挂起操作,在EntryI中也会有自旋和TryLock。等待队列是放在cxq的头部。

源码分析:
ObjectMonitor::enter

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;  //获取当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;//通过CAS操作,将拥有锁的线程变成自己,如果owner是null,就设置owner为自己
  if (cur == NULL) { //获取失败
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }
 //如果当前owner本来就是自己,重入次数+1
  if (cur == Self) {
     _recursions ++ ;
     return ;
  }
 
//如果当前线程是第一次进入monitor,代表是轻量级锁升级的,resource+1,线程owner为自己
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    // Commute owner from a thread-specific on-stack BasicLockObject address to
    // a full-fledged "Thread *".
    _owner = Self ;
    OwnerIsThread = 1 ; 
    return ;
  }
 

  Self->_Stalled = intptr_t(this) ;
  //TrySpin是一个自旋获取锁的操作
  if (Knob_SpinEarly && TrySpin (Self) > 0) {
     Self->_Stalled = 0 ;
     return ;
  }
  /*
  *省略部分代码
  */
    for (;;) {
// 如果上面操作失败了,就会进入EnterI,这里会进行挂起放入 _cxq 的操作,在里面有自旋和TryLock(Self)等操作,是为了避免挂起导致的性能损耗
      EnterI (THREAD) ;
      /**
      *省略了部分代码
      **/
     }
  }

复制代码

ObjectMonitor::exit调用流程 exit只会释放一次锁重入次数,如果_recursions为0了就表示要释放锁了,然后根据模式来唤醒等待线程。新加入的阻塞线程都在_cxq。关于EntryList和_cxq : 在模式2的情况下,唤醒是直接唤醒_cxq中的头部,在模式3则会将 _cxq 中的节点放到EntryList的头部,模式4会放到尾部,3和4的唤醒逻辑都是在EntryList中唤醒头部的线程

重量级锁自旋

在很多文章中把自旋单独列出来称为自旋锁,实际看源码知道,自旋是在重量级锁中为了避免被挂起而进行的优化,此时已经是重量级锁的逻辑了。

TrySpin_VaryDuration自旋:

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) { //也是在ObjectMonitor里面实现的逻辑,此时mark word指向的是ObjectMonitor对象

    // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.
    int ctr = Knob_FixedSpin ;//默认是0,如果配置了固定自旋次数则不为0,走while逻辑
    if (ctr != 0) {
        while (--ctr >= 0) {
        //TryLock(Self) 是通过CAS加锁的操作
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;  //此时时有一个自旋优化的操作
        }
        return 0 ;
    }

//Knob_PreSpin默认为10,没有配置自旋次数时会获取Knob_PreSpin,通过for循环自旋10次
for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {  
      if (TryLock(Self) > 0) {
        // Increase _SpinDuration ...
        // Note that we don't clamp SpinDuration precisely at SpinLimit.
        // Raising _SpurDuration to the poverty line is key.
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      SpinPause () ;
    }
复制代码

结语

感谢你能看到这里,这是我的第一篇博客,本来想将本文写成类似一问一答的形式,但是发现知识点太多一问一答会比较繁琐,所以按照传统博文方式写了。至此我们已经将synchronized所有知识点都过了一遍,结合源码和注释来学习知识脉络更加清晰。接下来会对volatile和AQS进行梳理,如果觉得本文对你有帮助,请点赞+关注,感谢!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享