🔴 Synchronized

吞佛童子2022年6月20日
  • Java
  • concurrency
大约 10 分钟

🔴 Synchronized

1. 用法

1) 修饰静态方法

  • 作用对象为: Class 类
  • 多线程并发访问时,只能有一个线程进入,获得类锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法
    public static synchronized void increase() {
        i++;
    }

2) 修饰实例方法

  • 作用对象为: 实例类
  • 多线程并发访问时,只能有一个线程进入,获得对象内置锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法
    public synchronized void increase(){
        i++;
    }

3) 修饰代码块

  • 作用对象为: 括号内的对象
  • 多线程并发访问时,只能有一个线程进入,根据括号中的对象或者是类,获得相应的对象内置锁 | 类锁
    //获得了String的类锁
    synchronized (String.class) {
        i++;
    }

每个类都有一个类锁,类的每个实例对象也有一个内置锁,它们是互不干扰的, 即,一个线程可以同时获得类锁和该类实例化对象的内置锁, 当线程访问非 synchronized 修饰的方法时,并不需要获得锁,因此不会产生阻塞


2. 底层原理

1) 同步方法

public class SynchronizedTest {
    public synchronized void doSth(){
        System.out.println("Hello World");
    }
}

javac SynchronizedTest .java 然后 javap -c SynchronizedTest 反编译后看汇编指令

 public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ☆☆☆
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  
         3: ldc           #3    
         5: invokevirtual #4                  
         8: return
  • 同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志
  • 当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁
  • 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放

2) 同步代码块

public class SynchronizedTest {
    public void doSth1(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }
}

javac SynchronizedTest .java 然后 javap -c SynchronizedTest 反编译后看汇编指令

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                 
         2: dup
         3: astore_1
         4: monitorenter  //   ☆☆☆
         5: getstatic     #2                  
         8: ldc           #3                  
        10: invokevirtual #4                
        13: aload_1
        14: monitorexit  //正常时 ☆☆☆
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit  // 异常时 ☆☆☆
        21: aload_2
        22: athrow
        23: return
  • 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0
  • 当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增
  • 当同一个线程释放锁(执行 monitorexit)的时候,计数器再自减。
  • 当计数器为0的时候,锁将被释放,其他线程便可以获得锁

3) monitor 原理解析

  • HotSpot 中,monitor 底层是由 ObjectMonitor 实现的,数据结构如下:
  • 位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件,C++ 实现
  • 源码地址open in new window
class ObjectMonitor {
  // 构造函数
  ObjectMonitor() {
    _header       = NULL; 
    _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 ;  //等待获取锁的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
  
    // 线程尝试获取锁  
    bool ObjectMonitor::try_enter(Thread* THREAD) {
      if (THREAD != _owner) { // 该对象锁之前不是本线程获取
        // 当前线程尝试获取 对象锁
        if (THREAD->is_lock_owned ((address)_owner)) { // 获取成功
           assert(_recursions == 0, "internal state error"); // 只能说明在此之前 锁的重入次数 == 0
           _owner = THREAD ; // 设置相关属性的值
           _recursions = 1 ;
           OwnerIsThread = 1 ;
           return true;
        }
        // 尝试修改 _owner 指针为 当前线程
        if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
          return false; // 修改失败,返回 false
        }
        return true; // 修改成功
      } else {
        // 本线程再次获取 对象锁
        _recursions++;
        return true;
      }
    }
    
    // 
    void ATTR ObjectMonitor::enter(TRAPS) {
      Thread * const Self = THREAD ;
      void * cur ;
    
      cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
      // 修改 _owner 指针为 当前线程,失败,返回
      if (cur == NULL) {
         assert (_recursions == 0   , "invariant") ;
         assert (_owner      == Self, "invariant") ;
         return ;
      }
      
      // 成功修改 _owner 指针为 当前线程
      if (cur == Self) {
         _recursions ++ ;
         return ;
      }
    
      if (Self->is_lock_owned ((address)cur)) {
        assert (_recursions == 0, "internal state error");
        _recursions = 1 ;
        _owner = Self ;
        OwnerIsThread = 1 ;
        return ;
      }
    
      // 我们遇到了真正的争论。
      assert (Self->_Stalled == 0, "invariant") ;
      Self->_Stalled = intptr_t(this) ;
    
      // 短暂 自旋
      if (Knob_SpinEarly && TrySpin (Self) > 0) {
         assert (_owner == Self      , "invariant") ;
         assert (_recursions == 0    , "invariant") ;
         assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
         Self->_Stalled = 0 ;
         return ;
      }
    
      assert (_owner != Self          , "invariant") ;
      assert (_succ  != Self          , "invariant") ;
      assert (Self->is_Java_thread()  , "invariant") ;
      JavaThread * jt = (JavaThread *) Self ;
      assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
      assert (jt->thread_state() != _thread_blocked   , "invariant") ;
      assert (this->object() != NULL  , "invariant") ;
      assert (_count >= 0, "invariant") ;
    
      // count ++
      Atomic::inc_ptr(&_count);
    
      EventJavaMonitorEnter event;
    
      { // 更改 java 线程状态以指示在监视器输入时被阻止。
        JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
    
        DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
        if (JvmtiExport::should_post_monitor_contended_enter()) {
          JvmtiExport::post_monitor_contended_enter(jt, this);
        }
    
        OSThreadContendState osts(Self->osthread());
        ThreadBlockInVM tbivm(jt);
    
        Self->set_current_pending_monitor(this);
    
        // TODO-FIXME:将以下 for(;;) 循环更改为直线代码。
        for (;;) {
          jt->set_suspend_equivalent();
          // 由 handle_special_suspend_equivalent_condition() 或 java_suspend_self() 清除
          EnterI (THREAD) ;
    
          if (!ExitSuspendEquivalent(jt)) break ;

          // 我们已经获得了竞争监视器,但是在我们等待时,另一个线程暂停了我们。我们不想在暂停时进入监视器,因为这会让暂停我们的线程感到惊讶。
          _recursions = 0 ;
          _succ = NULL ;
          exit (false, Self) ;  
          jt->java_suspend_self();
        }
        Self->set_current_pending_monitor(NULL);
      }
    
      Atomic::dec_ptr(&_count);
      assert (_count >= 0, "invariant") ;
      Self->_Stalled = 0 ;
    
      // Must either set _recursions = 0 or ASSERT _recursions == 0.
      assert (_recursions == 0     , "invariant") ;
      assert (_owner == Self       , "invariant") ;
      assert (_succ  != Self       , "invariant") ;
      assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
    
      // 线程——现在是所有者——又回到了 vm 模式。
      // 通过 TI、DTrace 和 jvmstat 报告这一喜讯。探测效应是不平凡的。
      // 所有的报道都是在我们拿着显示器的时候发生的,增加了关键部分的长度。 Amdahl 的并行加速定律生动地发挥了作用。
      //
      // 另一种选择可能是聚合事件(线程本地或每个监视器聚合)并将报告推迟到更合适的时间 - 
      // 例如下一次某个线程遇到争用但尚未获得锁。虽然旋转该线程可以旋转,但我们可以增加 JVMStat 计数器等。
    
      DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
      if (JvmtiExport::should_post_monitor_contended_entered()) {
        JvmtiExport::post_monitor_contended_entered(jt, this);
      }
    
      if (event.should_commit()) {
        event.set_klass(((oop)this->object())->klass());
        event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
        event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
        event.commit();
      }
    
      if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
         ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
      }
    }    
}
  • 底层方法流程:

img.png


3. 锁优化

JDK 6 进行锁的优化

锁升级流程

1) 偏向锁

  • 当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,
  • 偏向锁会永远偏向第一个获得锁的线程,
  • 如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁
  1. 参数
  • xx:UseBiasedLocking = true
  • JDK 1.5 关闭
  • JDK 1.6 默认开启
  • JDK 15 关闭废弃
  1. 适用场景
  • 连续多次是同一个线程申请相同的锁
  • 不适用于锁竞争激烈的场景
  1. 对象头
场景对象头
偏向锁还未偏向任何线程hashCode + 分代年龄 + 偏向标志位 1 + 锁标志位 01
偏向锁设置偏向某个线程线程 id + Epoch + 分代年龄 + 偏向标志位 1 + 锁标志位 01
  1. 源码 地址open in new window
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 使用了 偏向锁
 if (UseBiasedLocking) {
    // 不在 安全点,尝试偏向当前线程
    if (!SafepointSynchronize::is_at_safepoint()) {
      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 方法,进入轻量级状态
 slow_enter (obj, lock, THREAD) ;
}

2) 轻量级锁

  1. 适用场景
  • 竞争 锁对象 的线程不多 & 线程持有锁的时间也不长
  • 此时,自旋的消耗比内核态 - 用户态的切换开销小,时间短对 CPU 友好
  1. 对象头
场景对象头
轻量级锁指向 Record Lock 的指针 + 锁标志位 00
重量级锁指向 重量级锁 的指针 + 锁标志位 01
  1. 原理
  • 在当前线程的栈帧中建立 Record Lock 作为 锁对象头 Mark Word 的副本
  • CAS 将 锁对象的 Mark Word 更新为指向 Record Lock 的地址
  • 若成功,修改对象头的锁标志位为 00,表示对象处于轻量级锁状态,线程获取了这个对象的锁,执行相关操作,执行完同步操作后,将 Mark Word 指针复位
  • 若失败,说明有其他线程在和当前线程竞争该对象的锁,此时尝试自旋等待其他线程释放锁,若自旋一定次数后仍未获得锁,则升级为重量级锁
  1. 源码
// 不允许使用偏向锁 | 获偏向锁失败,进入该方法
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()) { // 未被其他线程获取
    // 创建副本
    lock->set_displaced_header(mark);
    // CAS 尝试更新 锁对象的对象头
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ; // 成功,返回
    }
    // Fall through to inflate() ...
  } 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
  // 重置 lock.set_displaced_header
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif

  lock->set_displaced_header(markOopDesc::unused_mark());
  // 升级为 重量级锁
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

自旋

  • 自旋锁:
    • JDK 1.4.2 引入自旋锁,默认关闭
    • JDK 1.6 自旋锁默认开启, -XX:+UseSpinning 开启, +XX:PreBlockSpin = 10 [default = 10]
    • 自适应自旋锁,由 JVM 进行控制,取决于:
      • 当前线程上一次获得该锁的自旋时间
      • 当前锁的拥有者状态

3) 锁消除

  • 虚拟机即时编译器在运行时检测到某段需要同步的代码不可能存在共享数据竞争,因此对锁进行消除
  • 主要判定依据:逃逸分析

4) 锁粗化

  • 一系列的连续操作都是对同一对象进行反复加锁 & 解锁操作,此时可以将加锁同步的范围扩展

4. 特点

1) 有序性

  • 保证多线程任意时刻只能有一个线程获取锁对象,相当于单线程执行,而由于单线程的 as-if-serial 原则,保证了有序性

2) 可见性

  • 加锁之前将工作内存的变量值失效,从主存中读取,释放锁之前,将工作内存的变量刷回主存,从而保证了可见性

3) 原子性

  • 同一时刻单线程执行保证原子性

4) 可重入性

  • 为可重入锁,有效避免死锁的情况,可同一线程多次获得同一个对象锁

5) 独占性

  • 为独占锁,任意时刻最多一个线程占用

6) 不可中断性

  • 一个线程获取到锁对象后,其余想要获取该锁的线程只能进入等待 / 阻塞状态,不可被中断

7) 非公平锁

  • 当锁被某个线程释放,其余线程可以获取锁时,其余线程共同竞争,而不是按照先后顺序获取

8) 自动释放锁

  • 当线程不需要锁对象时,自动释放,无需手动释放
上次编辑于: 2022/10/10 下午8:43:48
贡献者: liuxianzhishou