<返回更多

从C源码看Java同步锁机制的演变

2023-11-07  今日头条  代码小人物
加入收藏

什么是重量级锁?

重量级锁是一种同步机制,通常与在多线程环境中使用synchronized关键字实现同步相关。

由于其实现的开销和复杂性较高,因此被称为“重量级”,适合需要更严格的同步和并发控制的场景。

private synchronized void oneLock() {
    //doSomething();
}

两个线程t1和t2正在同时访问该oneLock()方法。如果t1先获取锁并执行其中的同步代码块,并且 t2 也尝试访问oneLock() 方法,则它将被阻止,因为锁由t1 持有。

在这种情况下,锁处于称为重量级锁的状态。

从上面的例子可以看出,t2由于无法获取锁,因此被挂起,等待t1释放锁后再被唤醒。

线程的挂起和唤醒涉及CPU内的上下文切换,这会产生很大的开销。

由于这个过程的成本相对较高,具有这种行为的锁被称为重量级锁。

什么是轻量级锁

轻量级锁是一种同步机制,旨在减轻与传统重量级锁(例如 JAVA synchronized关键字提供的锁)相关的性能开销。

继续前面的示例,让我们现在考虑t1和t2交替执行oneLock()方法。

在这种情况下,t1和t2不需要阻塞,因为它们之间没有争用。换句话说,不需要重量级的锁。

当线程交替执行临界区而不发生争用时,这种场景下使用的锁被称为轻量级锁。

轻量级锁相对于重量级锁的优点:

1、每次加锁只需要一次CAS操作。
2. 无需分配ObjectMonitor对象。
3、线程不需要被挂起或唤醒。

什么是偏向锁?

在只有一个线程(假设 t1)一致执行oneLock()方法的情况下,使用轻量级锁t1在每次获取锁时执行 CAS 操作。这可能会导致一些性能开销。

于是,偏向锁的概念就出现了。

当锁偏向特定线程时,该线程可以再次获取锁,而无需进行 CAS 操作。相反,简单的比较就足以获得锁。这个过程非常高效。

偏向锁相比轻量级锁的优点:

怎样加锁?

让我们从源代码的角度深入研究一下 Java 中这些锁是如何实现的。

锁的本质在于共享变量,所以问题的关键是如何访问这些共享变量。了解这一点就了解了这三种锁的演变过程的一半。

接下来我将从源码分析的角度重点介绍一下这些信息。

既然我们处理的是锁,自然就涉及到锁的获取和释放操作,而在偏向锁的情况下,还有锁撤销操作。

对象头是Java对象在内存中布局的一部分,用于存储对象的元数据信息和锁定状态。

从C源码看Java同步锁机制的演变

 

在深入源码之前,我们先推测一下线程 t1 获取偏向锁的过程:

  1. 首先检查Mark word中的线程ID是否有值。
  2. 如果没有,则意味着还没有线程获得锁。本例中,直接将t1的线程ID记录到Mark Word中。多个线程可能会尝试同时修改Mark Word,因此需要CAS操作来修改Mark Word。
  3. 如果已经有一个 ID 值,那么有两种可能性:
CASE(_monitorenter): {
// 1. 获取对象头,表示为“oop”(普通对象指针)。
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  BasicObjectLock* entry = NULL;
  while (most_recent != limit ) {
// 2. 遍历线程栈找到对应的可用BasicObjectLock。  
    if (most_recent->obj() == NULL) entry = most_recent;
    else if (most_recent->obj() == lockee) break;
    most_recent++;
  }
  if (entry != NULL) {
// 3. BasicObjectLock 的 _obj 字段指向 oop。
    entry->set_obj(lockee);
    int success = false;
    uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;

// 从对象头中检索标记
    markOop mark = lockee->mark();
    intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 检查是否支持偏向锁定。
    if (mark->has_bias_pattern()) {
      uintptr_t thread_ident;
      uintptr_t anticipated_bias_locking_value;
      thread_ident = (uintptr_t)istate->thread();
// 4. 获取异或运算的结果。
      anticipated_bias_locking_value =
        (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
        ~((uintptr_t) markOopDesc::age_mask_in_place);

      if  (anticipated_bias_locking_value == 0) {
// 5. 如果相等,则认为是可重入获取锁。
        if (PrintBiasedLockingStatistics) {
          (* BiasedLocking::biased_lock_entry_count_addr())++;
        }
        success = true;
      }
      else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// 6. 如果不支持偏向锁
        markOop header = lockee->klass()->prototype_header();
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
// 执行CAS操作,将Mark Word修改为解锁状态。
        if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (*BiasedLocking::revoked_lock_entry_count_addr())++;
        }
      }
      else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 7. 如果epoch已过期,则使用当前线程的ID构造偏向锁
        markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
        if (hash != markOopDesc::no_hash) {
          new_header = new_header->copy_set_hash(hash);
        }
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::rebiased_lock_entry_count_addr())++;
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
      else {
// 8. 构造一个匿名偏向锁。
        markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
                                                        (uintptr_t)markOopDesc::age_mask_in_place |
                                                        epoch_mask_in_place));
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
// 构造一个指向当前线程的偏向锁。
        markOop new_header = (markOop) ((uintptr_t) header | thread_ident);

        DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
// 执行CAS操作将锁修改为与当前线程关联的偏向锁。      
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
        }
        else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
    }

    if (!success) {
// 如果尝试使用偏向锁不成功,系统会尝试将锁升级为轻量级锁。
      markOop displaced = lockee->mark()->set_unlocked();
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

代码比较多,下面我将对代码注释中注释1-8标注的内容进行详细解释。

# 1.oop代表对象头,包含Mark Word和Klass Word。

# 2.BasicObjectLock的结构如下:

#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                  
  oop       _obj;                                   
  ...
};

class BasicLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  volatile markOop _displaced_header;
  ...
};

BasicObjectLock是著名的Lock Record的实现,它包括两个元素:

  1. 存储Mark Word的移位头_displaced_header。
  2. 指向对象头的指针:_obj。

# 3、将Lock Record中的_obj字段赋值给lockee,代表对象头。

# 4. 从对象头lockee中,检索Klass Word,它是指向Klass类型的指针。在Klass类内部,有一个名为_prototype_header的字段,它也代表Mark Word。它存储偏向锁定标志之类的信息。

在此步骤中,提取此信息并将其与当前线程 ID 连接起来。

然后与对象头中的Mark Word 执行XOR 运算。目标是识别不同的位。

后续步骤涉及确定Mark Word的哪些特定部分不相等,从而导致不同的处理逻辑。

# 5. 如果上面的异或运算结果相等,则表明Mark Word中包含当前线程ID,并且epoch和偏向锁标志一致。

这表明该锁已经被当前线程持有,表明是可重入的。由于线程已经拥有锁,因此不需要采取进一步的操作。

# 6. 观察Mark Word中的偏向锁标志与Klass中的偏向锁标志不一致,并且考虑到Mark Word已经被识别为具有偏向锁,因此可以推断Klass不再支持偏向锁。

鉴于不支持偏向锁定,标记字被修改以反映解锁状态。这为进一步升级到轻量级锁定或重量级锁定做好了准备。

# 7. 在识别出 Mark Word 中的纪元与 Klass 中的标记之间的差异后,可以推断发生了批量重新偏置。这种情况下,直接修改Mark Word,使其偏向当前线程。

# 8、如果以上条件都不满足,则表明是匿名偏向锁(不偏向任何线程的偏向锁)。在这种情况下,会尝试直接修改Mark Word以偏向当前线程

总结

在偏向锁状态下,锁记录和对象头之间建立了关系。这种关系由指向对象头的锁定记录的 _obj 字段表示。

从C源码看Java同步锁机制的演变

 

我们回顾一下线程t1和t2获取偏向锁的过程:

  1. 线程 t1 尝试获取锁。最初,锁处于匿名偏向状态, T1成功获取锁。
  2. 线程 t1 尝试再次获取锁。由于它已经持有锁,所以他会来获取可重入锁。
  3. 同时,线程 t2 尝试获取锁。由于 t1 当前持有锁定,因此 t2 会锁撤销。

锁撤销

如果尝试获取偏向锁不成功,锁将恢复为未锁定状态,然后升级为轻量级锁。此过程称为偏向锁撤销。

#InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  if (UseBiasedLocking) {
// 当使用偏向锁时,进程进入快速路径执行。
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
// 升级为轻量级锁。
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ...

#synchronizer.cpp
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 (obj, lock, THREAD) ;
}

可见,撤销分为安全点撤销和非安全点撤销。

非安全点撤销,也称为“revoke_and_rebias”,发生在未等待安全点而撤销偏向锁时。在这个过程中,偏向锁被直接撤销,并且对象的标记字被更新以反映新的状态,而不需要安全点来保证一致的状态转换。

当发生非安全点撤销时,偏向锁的状态从偏向变为正常或可重偏向。

如果它更改为可重偏向状态,则意味着如果另一个线程寻求该锁,该锁可以再次偏向。

这允许更快、更有效的锁定转换,因为如果另一个线程在撤销后不久获取该锁,则该锁可能会跳过中间状态并直接进入偏向状态。

从本质上讲,非安全点撤销减少了等待安全点的需要,并实现了更灵活、响应更灵敏的方法来撤销偏向锁,从而提高了性能并减少了某些场景下的锁争用。

批量重新偏向和批量撤销

经过以上分析,我们了解到以下几点:

因此,偏向锁引入了批量重偏向和批量撤销的概念。

当对象的锁被撤销的次数达到一定阈值时,例如20次,就会触发批量重偏逻辑。

这涉及到修改 Klass 中的标记以及当前使用的该类型锁的 Mark Word 中的标记。

当线程尝试获取偏向锁时,它会将当前对象的纪元值与 Klass 中的标记值进行比较。

如果不相等,则认为锁已过期。在这种情况下,允许线程直接CAS修改Mark Word以偏向当前线程,避免撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(7)。

同样,当撤销次数达到40次时,就认为该对象不再适合偏向锁。

因此,Klass 中的偏向锁标志发生更改,以指示不再支持偏向锁。

当线程尝试获取偏向锁时,它会检查 Klass 中的偏向锁标志。如果不再允许偏差,则表明批次撤销较早发生。

在这种情况下,允许线程直接CAS将Mark Word修改为解锁状态,避免了撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(6)。

批量重新偏向和批量撤销是旨在提高偏向锁定性能的优化。

锁释放


#bytecodeInterpreter.cpp
CASE(_monitorexit): {
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 遍历线程栈
  while (most_recent != limit ) {
// 查找对应的锁记录
    if ((most_recent)->obj() == lockee) {
      BasicLock* lock = most_recent->lock();
      markOop header = lock->displaced_header();
// 将锁定记录中的_obj字段设置为null
      most_recent->set_obj(NULL);
// 这是轻量级锁的释放。
      UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
    }
    most_recent++;
  }
  ...
}

您可能已经注意到,Mark Word 没有改变;它仍然偏向于前一个线程。然而,锁还没有被释放。事实上,当线程退出临界区时,它不会释放偏向锁。

原因是:

当再次需要锁时,简单的按位比较就可以快速判断是否是可重入获取。这意味着不需要每次都执行CAS操作就可以高效地获取锁。这种效率是偏向锁在只有一个线程访问锁的场景下的核心优势。

总结

  1. 偏向锁中的“锁”指的是Mark Word。修改Mark Word是获取锁所必需的,由于潜在的多线程争用,这可能会涉及CAS操作。
  2. 由于撤销操作在安全点执行时效率可能较低,并且多次撤销会进一步影响效率,因此引入了批量重偏和撤销机制。
  3. 偏向锁的可重入计数取决于线程堆栈中存在的锁记录的数量。
  4. 如果偏向锁撤销失败,锁最终会升级为轻量级锁。
  5. 退出时,偏向锁不会修改Mark Word,也就是说锁没有被释放。

未完待续。。。。。

关键词:Java      点击(5)
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多Java相关>>>