Skip to content
Go back

synchronized锁升级机制

synchronized 锁升级:从偏向锁到重量级锁的完整状态机

一句话结论(30s)

synchronized 在 JDK 6 之后已经不是「重锁」,而是根据竞争程度自动升级的四级状态机(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)。它的关键设计是把锁的全部状态编码进对象头的 64 位 Mark Word——偏向态存线程 ID、轻量级态存 Lock Record 指针、重量级态存 Monitor 指针,因为无竞争时一次 CAS 就能完成加锁、有竞争才逐步膨胀到 OS 互斥量,从而做到「无竞争近零开销、有竞争才付重代价」。核心权衡是偏向锁的 STW 撤销成本——正因为现代 CPU 上这个成本已大于 CAS 节省的开销,JDK 15 才默认禁用、JDK 18 彻底移除了偏向锁。

核心原理(2min)

主流程:加锁时 JVM 先尝试偏向锁——检查 Mark Word 里记录的线程 ID 是否是自己,是则零开销直接进入,否则 CAS 写入自己的线程 ID;CAS 失败说明有其他线程竞争,于是 STW 撤销偏向锁、升级为轻量级锁——在当前线程栈帧分配 Lock Record、备份原 Mark Word、再 CAS 把 Mark Word 换成指向 Lock Record 的指针,全程用户态完成、不进内核。只有当 CAS 失败且非本线程重入(真正的竞争)时才「膨胀」为重量级锁:在堆上分配 ObjectMonitor,竞争线程挂到 _EntryList 用 pthread_cond_wait 阻塞。关键机制有三:锁升级不可逆(来回切换开销更大);JIT 的锁消除与锁粗化进一步减少无谓加锁;以及 hashCode() 与偏向锁互斥,因为偏向态 54 位都存线程 ID、没有位置放 hashCode。

底层深入(5-10min)

synchronized 不是一直都是”重锁”

JDK 1.6 之前,synchronized 直接走 OS 的 pthread_mutex_lock(重量级锁),每次加锁都从用户态切换到内核态,开销极大。但实际场景中大部分锁根本不存在竞争——同一个线程反复获取同一把锁,或者两个线程交替获取但从不等待。

JDK 1.6 引入了锁升级机制:锁根据竞争程度自动升级,无竞争时几乎零开销,有竞争时才逐步升级到重量级锁。

想一想:为什么需要「锁升级」,而不是一开始就直接用重量级锁? 因为大多数锁在实际中根本不存在竞争——同一个线程反复获取,或两个线程交替获取但从不等待。如果一律走内核态的 OS 互斥量,每次加锁都是一次用户态↔内核态切换,无竞争也要白白付出昂贵的系统调用代价。所以正确策略是「按需付费」:先用最便宜的用户态 CAS 试探,只有真正出现竞争才逐步膨胀到内核态阻塞——这就是锁升级的本质权衡:用一套更复杂的状态机,换无竞争场景的近零开销。

Mark Word:64 bit 里藏着锁的全部状态

HotSpot 64 位对象头中,Mark Word 的低 3 bit 是锁状态标志位,不同状态下整 64 bit 的布局完全不同:

无锁态(001):
  [ unused:25 | hashCode:31 | unused:1 | age:4 | biased_lock:0 | 01 ]

偏向锁(101):
  [ thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 ]

轻量级锁(00):
  [ ptr_to_lock_record:62 | 00 ]

重量级锁(10):
  [ ptr_to_monitor:62 | 10 ]

GC 标记(11):
  [ forward_ptr:62 | 11 ]

关键信息全部编码在这 64 bit 中——偏向锁时存线程 ID,轻量级锁时存 Lock Record 指针,重量级锁时存 Monitor 对象指针。

想一想:为什么要把锁状态编码进 Mark Word,而不是给每个对象单独分配一个锁字段? 因为对象头是每个对象都有的固定开销,锁状态本来就该「就地」存——不额外占内存、也无需查表。用低 2~3 bit 做状态标志位,其余位按状态复用(存线程 ID / Lock Record 指针 / Monitor 指针),相当于用一个 64 位寄存器装下了整个锁状态机。代价是不同状态要「复用同一块内存」,所以才会有下文 hashCode 与偏向锁互斥这类取舍。

上面这张 5 态布局是 JDK 6~14 的历史版本。当前 JDK(本文所引源码的版本)已彻底移除偏向锁、并把轻量级锁换成「快锁」,Mark Word 低 2 bit 只剩 3 种锁状态 + 1 种 GC 状态。真实源码(oops/markWord.hpp)的位布局与锁状态常量如下:

// 64 位对象头的位布局(大端,最高有效位在前)
//
//  64 bits (without compact headers):
//  ----------------------------------
//  unused:22  hash:31  valhalla:4  age:4  self-fwd:1  lock:2
//
//  - lock bits are used to describe lock states: locked/unlocked/monitor-locked
//    and to indicate that an object has been GC marked / forwarded.
//
//    [header          | 00]  locked             locked regular object header (fast-locking in use)
//    [header          | 01]  unlocked           regular object header
//    [header          | 10]  monitor            inflated lock
//    [ptr             | 11]  marked             used to mark an object (header is swapped out)
  // 锁状态位的取值(最低有效位顺序)
  static const uintptr_t locked_value             = 0;
  static const uintptr_t unlocked_value           = 1;
  static const uintptr_t monitor_value            = 2;
  static const uintptr_t marked_value             = 3;
  // 同步代码里所有「锁升级判断」的唯一起点:读低 2 bit 判断状态
  bool is_unlocked() const {
    return (mask_bits(value(), lock_mask_in_place) == unlocked_value);
  }

  bool is_fast_locked() const {
    return (value() & lock_mask_in_place) == locked_value;
  }
  markWord set_fast_locked() const {
    // Clear the lock_mask_in_place bits to set locked_value:
    return markWord(value() & ~lock_mask_in_place);
  }

  bool has_monitor() const {
    return ((value() & lock_mask_in_place) == monitor_value);
  }
  markWord set_has_monitor() const {
    return markWord((value() & ~lock_mask_in_place) | monitor_value);
  }

新版 Mark Word 的锁状态只有低 2 bit:00 是 locked(快锁,对应旧的轻量级锁,用线程内 LockStack 代替栈上 Lock Record)、01 是 unlocked(无锁)、10 是 monitor(已膨胀的重量级锁)、11 是 marked(GC 标记)。is_unlocked()/is_fast_locked()/has_monitor() 这三个谓词是加锁路径的第一步——先读 Mark Word 判断当前处于哪一态,再决定走快锁 CAS 还是膨胀到 ObjectMonitor。偏向锁的 thread:54 布局之所以消失,正是因为 JDK 15 默认禁用、JDK 18 彻底移除后,HotSpot 把 Mark Word 简化回了 2 bit 锁位。

状态一:偏向锁(Biased Locking)

核心思想:大多数锁从未被多个线程竞争,且同一个线程反复获取。

偏向锁用一次 CAS 在 Mark Word 中记录当前线程 ID(54 bit):

加锁流程:
1. 检查 Mark Word 中 biased_lock=1 且 thread_id 指向自己 → 已偏向自己,直接进入(零开销)
2. 否则 CAS 将 Mark Word 改为 thread_id=当前线程 + biased_lock=1
3. CAS 成功 → 获得偏向锁
4. CAS 失败 → 偏向被其他线程持有 → 撤销偏向锁 → 升级为轻量级锁

撤销偏向锁是 STW(Stop-The-World)操作——JVM 需要在安全点暂停所有线程,检查持有偏向锁的线程是否还活着,然后修改 Mark Word 状态。这是偏向锁”不便宜”的根本原因。

JDK 15 起默认禁用偏向锁,JDK 18 完全移除。 原因:现代硬件 CAS 已经很快,偏向锁的 STW 撤销成本大于 CAS 节省的开销。

想一想:偏向锁明明要付出 STW 撤销的成本,为什么曾经还要用它? 因为在 JDK 6 那个年代,CAS 指令本身很贵,而「单线程反复获取同一把锁」又是最常见场景——偏向锁让这种情况的加锁成本降到「读一次 Mark Word 比对线程 ID」,连 CAS 都省了。它赌的是「锁大概率只有一个线程用」,只有当第二个线程真的来抢时才付出 STW 撤销的代价:赌赢了近乎零开销,赌输了才一次性结算。这正是一种乐观设计,也正因为它赌输后的结算成本随硬件演进越来越不划算,才最终被禁用、被移除。

状态二:轻量级锁(Lightweight Locking)

核心思想:两个线程交替获取锁,不产生阻塞。

加锁时在当前线程的栈帧中分配一块 Lock Record,然后将 Mark Word 备份到 Lock Record 的 displaced_header 字段,最后 CAS 将 Mark Word 替换为指向 Lock Record 的指针:

线程栈帧:
  LockRecord.displaced_header = 对象原始 MarkWord(备份)
  LockRecord.owner = 指向锁对象

加锁:
  CAS(obj.markWord, 原始值, 指向LockRecord的指针)
  成功 → 持有轻量级锁
  失败 → 判断是否本线程重入 → 是: displaced_header 置 null(标记重入)
                               否: 膨胀为重量级锁

重入判断:检查 Mark Word 中 Lock Record 指针是否指向当前线程的栈空间——不需要计数器,靠 Lock Record 链表上的 null displaced_header 来标记重入层数。

轻量级锁完全在用户态完成,不进内核。 只有 CAS 失败且非重入时(真正的竞争),才升级到重量级锁。

想一想:轻量级锁靠 CAS 自旋,为什么只有两个线程「交替」获取才划算? 因为自旋的线程在空转烧 CPU。两个线程交替获取时,锁很快就会被释放,自旋几次就能拿到,省去了阻塞 + 唤醒的两次上下文切换;可一旦是真正的竞争(一个线程长时间持锁),自旋线程会一直空转到白烧 CPU 还拿不到锁,此时再自旋就不如直接挂起等 OS 调度。所以「自旋是否划算」的分界线就是临界区的长短——这也是为什么下面源码里会先 fast_lock_spin_enter 自旋、失败才膨胀到 ObjectMonitor。

当前 JDK 里「轻量级锁」已被「快锁(fast-locking)」取代:不再在栈帧分配 Lock Record 备份 displaced header,而是 CAS 把 Mark Word 置为 locked(00)后,把对象推进线程本地的 LockStack。真实源码(runtime/synchronizer.cpp)的快锁 CAS 如下:

inline bool ObjectSynchronizer::fast_lock_try_enter(oop obj, LockStack& lock_stack, JavaThread* current) {
  markWord mark = obj->mark();
  while (mark.is_unlocked()) {
    ensure_lock_stack_space(current);
    assert(!lock_stack.is_full(), "must have made room on the lock stack");
    assert(!lock_stack.contains(obj), "thread must not already hold the lock");
    // Try to swing into 'fast-locked' state.
    markWord locked_mark = mark.set_fast_locked();
    markWord old_mark = mark;
    mark = obj->cas_set_mark(locked_mark, old_mark);
    if (old_mark == mark) {
      // Successfully fast-locked, push object to lock-stack and return.
      lock_stack.push(obj);
      return true;
    }
  }
  return false;
}

while (mark.is_unlocked()) 循环只做一件事:不断尝试用 cas_set_mark 把无锁态(01)CAS 成快锁态(00),成功后把对象压入当前线程的 LockStack。重入不再靠 displaced_header 置 null 计数,而是 lock_stack.contains(obj) 判断本线程是否已持有该对象。全程用户态 CAS,无锁竞争时零阻塞。

而锁升级的「梯子」在 ObjectSynchronizer::enter 里串起来,真实源码如下:

void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) {
  assert(current == JavaThread::current(), "must be");

  if (obj->klass()->is_value_based()) {
    ObjectSynchronizer::handle_sync_on_value_based_class(obj, current);
  }

  CacheSetter cache_setter(current, lock);

  // Used when deflation is observed. Progress here requires progress
  // from the deflator. After observing that the deflator is not
  // making progress (after two yields), switch to sleeping.
  SpinYield spin_yield(0, 2);
  bool observed_deflation = false;

  LockStack& lock_stack = current->lock_stack();

  if (!lock_stack.is_full() && lock_stack.try_recursive_enter(obj())) {
    // Recursively fast locked
    return;
  }

  if (lock_stack.contains(obj())) {
    ObjectMonitor* monitor = inflate_fast_locked_object(obj(), ObjectSynchronizer::inflate_cause_monitor_enter, current, current);
    bool entered = monitor->enter(current);
    assert(entered, "recursive ObjectMonitor::enter must succeed");
    cache_setter.set_monitor(monitor);
    return;
  }

  while (true) {
    // Fast-locking does not use the 'lock' argument.
    // Fast-lock spinning to avoid inflating for short critical sections.
    // The goal is to only inflate when the extra cost of using ObjectMonitors
    // is worth it.
    // If deflation has been observed we also spin while deflation is ongoing.
    if (fast_lock_try_enter(obj(), lock_stack, current)) {
      return;
    } else if (fast_lock_spin_enter(obj(), lock_stack, current, observed_deflation)) {
      return;
    }

    if (observed_deflation) {
      spin_yield.wait();
    }

    ObjectMonitor* monitor = inflate_and_enter(obj(), lock, ObjectSynchronizer::inflate_cause_monitor_enter, current, current);
    if (monitor != nullptr) {
      cache_setter.set_monitor(monitor);
      return;
    }

    // If inflate_and_enter returns nullptr it is because a deflated monitor
    // was encountered. Fallback to fast locking. The deflater is responsible
    // for clearing out the monitor and transitioning the markWord back to
    // fast locking.
    observed_deflation = true;
  }
}

这段 enter 精确展示了升级梯子:(1) 若对象已在本线程 LockStack,try_recursive_enter 直接重入返回;(2) 否则 fast_lock_try_enter 先做一次快锁 CAS;(3) 失败则 fast_lock_spin_enter 用指数退避自旋,避免短临界区也去膨胀;(4) 自旋仍失败(真正的竞争)才 inflate_and_enter 膨胀为 ObjectMonitor。这就是现代版「无锁 → 轻量(快锁+自旋)→ 重量」的完整升级链。

状态三:重量级锁(Heavyweight Lock)

核心思想:真正的竞争发生,必须用 OS 互斥量。

升级到重量级锁的过程叫”膨胀”(Inflation):

  1. 在堆上分配 ObjectMonitor 对象(C++ 实现)
  2. ObjectMonitor._owner 设置为当前持锁线程
  3. 将 Mark Word 改为指向 ObjectMonitor 的指针(低 2 bit = 10)
  4. 其他竞争线程挂到 ObjectMonitor 的 _EntryList(双向链表),调用 pthread_cond_wait 阻塞
ObjectMonitor 结构:
  _owner       → 当前持锁线程(或 null)
  _EntryList   → 等待获取锁的线程队列(竞争失败)
  _WaitSet     → 调了 wait() 的线程队列
  _recursions  → 重入计数

真实源码(runtime/objectMonitor.hpp)里的关键字段如下:

  static const int64_t NO_OWNER = 0;
  static const int64_t ANONYMOUS_OWNER = 1;
  static const int64_t DEFLATER_MARKER = 2;

  int64_t volatile _owner;  // Either owner_id of owner, NO_OWNER, ANONYMOUS_OWNER or DEFLATER_MARKER.
  volatile uint64_t _previous_owner_tid;  // thread id of the previous owner of the monitor
  // Separate _owner and _next_om on different cache lines since
  // both can have busy multi-threaded access. _previous_owner_tid is only
  // changed by ObjectMonitor::exit() so it is a good choice to share the
  // cache line with _owner.
  DEFINE_PAD_MINUS_SIZE(1, OM_CACHE_LINE_SIZE, sizeof(void* volatile) +
                        sizeof(volatile uint64_t));
  ObjectMonitor* _next_om;          // Next ObjectMonitor* linkage
  volatile intx _recursions;        // recursion count, 0 for first entry
  ObjectWaiter* volatile _entry_list;  // Threads blocked on entry or reentry.
                                       // The list is actually composed of wait-nodes,
                                       // acting as proxies for Threads.
  ObjectWaiter* volatile _entry_list_tail; // _entry_list is the head, this is the tail.
  int64_t volatile _succ;           // Heir presumptive thread - used for futile wakeup throttling

  volatile int _SpinDuration;

  int _contentions;                 // Number of active contentions in enter(). It is used by is_busy()
                                    // along with other fields to determine if an ObjectMonitor can be
                                    // deflated. It is also used by the async deflation protocol. See
                                    // ObjectMonitor::deflate_monitor().
  int64_t _unmounted_vthreads;      // Number of nodes in the _entry_list associated with unmounted vthreads.
                                    // It might be temporarily more than the actual number but never less.
  OopHandle _object_strong;         // Used to protect object during preemption on class initialization

  ObjectWaiter* volatile _wait_set; // LL of threads waiting on the monitor - wait()
  volatile int  _waiters;           // number of waiting threads
  volatile int _wait_set_lock;      // protects wait set queue - simple spinlock
  volatile int _object_strong_lock; // protects setting of _object_strong

_owner 存的是持锁线程的 owner_id(NO_OWNER=0 无主、ANONYMOUS_OWNER=1 刚膨胀还没归属、DEFLATER_MARKER=2 正在被异步降级回收),而不再是直接存线程指针。_entry_list 是竞争失败线程挂起的双向链表(节点是 ObjectWaiter,充当线程代理),_wait_set 则是调了 wait() 的线程队列。注释里反复出现的 cache line 分隔(DEFINE_PAD_MINUS_SIZE)是为了避免 _owner 的 CAS 写入导致其它热字段的伪共享失效——重量级锁的性能敏感点就在这里。

锁膨胀的触发时机

// 线程 A 持有轻量级锁,线程 B 尝试 CAS 失败
线程 B:
  CAS(obj.markWord, 期望轻量, LockRecord指针) 失败
  → 检查是否本线程重入 → 否
  → 不是重入!发生了真正竞争
  → 调 ObjectSynchronizer::inflate()
    → 分配 ObjectMonitor
    → 将 A 的 LockRecord 链转换为 Monitor 的 owner/recursions
    → 将 B 挂到 EntryList
    → 修改 markWord → 指向 Monitor(重量级锁标志=10

真正的膨胀逻辑在 ObjectSynchronizer::inflate_and_enter 里,其核心是对 Mark Word 三种状态的逐一分派,真实源码如下:

  for (;;) {
    const markWord mark = object->mark_acquire();
    // The mark can be in one of the following states:
    // *  inflated     - If the ObjectMonitor owner is anonymous
    //                   and the locking_thread owns the object
    //                   lock, then we make the locking_thread
    //                   the ObjectMonitor owner and remove the
    //                   lock from the locking_thread's lock stack.
    // *  fast-locked  - Coerce it to inflated from fast-locked.
    // *  neutral      - Inflate the object. Successful CAS is locked

    // CASE: inflated
    if (mark.has_monitor()) {
      LockStack& lock_stack = locking_thread->lock_stack();
      if (monitor->has_anonymous_owner() && lock_stack.contains(object)) {
        // The lock is fast-locked by the locking thread,
        // convert it to a held monitor with a known owner.
        monitor->set_owner_from_anonymous(locking_thread);
        monitor->set_recursions(lock_stack.remove(object) - 1);
      }

      break; // Success
    }

    // CASE: fast-locked
    // Could be fast-locked either by locking_thread or by some other thread.
    //
    if (mark.is_fast_locked()) {
      markWord old_mark = object->cas_set_mark(mark.set_has_monitor(), mark);
      if (old_mark != mark) {
        // CAS failed
        continue;
      }

      // Success! Return inflated monitor.
      LockStack& lock_stack = locking_thread->lock_stack();
      if (lock_stack.contains(object)) {
        // The lock is fast-locked by the locking thread,
        // convert it to a held monitor with a known owner.
        monitor->set_owner_from_anonymous(locking_thread);
        monitor->set_recursions(lock_stack.remove(object) - 1);
      }

      break; // Success
    }

    // CASE: neutral (unlocked)

    // Catch if the object's header is not neutral (not locked and
    // not marked is what we care about here).
    assert(mark.is_neutral(), "invariant: header=" INTPTR_FORMAT, mark.value());
    markWord old_mark = object->cas_set_mark(mark.set_has_monitor(), mark);
    if (old_mark != mark) {
      // CAS failed
      continue;
    }

    // Transitioned from unlocked to monitor means locking_thread owns the lock.
    monitor->set_owner_from_anonymous(locking_thread);

    return monitor;
  }

三个 CASE 分支正好对应 Mark Word 的三种锁状态:has_monitor()(10)说明已经膨胀过,直接复用;is_fast_locked()(00)用 set_has_monitor() 的 CAS 把快锁强制「推」进 monitor 态;is_neutral()(01)从无锁直接 CAS 到 monitor。这就是「膨胀」的真实实现——不再是老版本里把 Lock Record 链搬进 monitor,而是靠 set_has_monitor() 改 Mark Word 低 2 bit、配合 set_owner_from_anonymous 认领归属,CAS 竞争失败就 continue 重试。

锁升级不可逆

一旦从轻量级膨胀为重量级,就不会再降回来。原因:如果在竞争激烈的环境下反复升降,来回切换的开销比一直保持重量级锁还大。

想一想:为什么膨胀后就「回不去」了? 因为膨胀本身已经把 Mark Word 改成了 Monitor 指针、在堆上分配了 ObjectMonitor,若再降级回去又要撤销 Monitor、改回 CAS 路径。在竞争激烈的环境下,线程大概率还会再竞争,来回升降的切换成本比一直保持重量级更大。所以 JVM 选择单向升级:宁可让竞争场景一直停在重量级,也不做无谓的来回折腾。

为什么 hashCode() 后不能用偏向锁

偏向锁 Mark Word 中 ThreadID 占 54 bit,没有位置存 hashCode(需要 31 bit)。一旦调用 Object.hashCode(),JVM 必须将 hashCode 写入 Mark Word 的低 31 bit——此时只能撤销偏向锁退回到无锁状态。

想一想:为什么 hashCode 会跟偏向锁「互斥」,而不是把 hashCode 挪到别处存? 因为偏向态的 Mark Word 里 54 bit 全被线程 ID 占满,已经没有位置放 31 bit 的 hashCode;而 hashCode 一旦生成就必须稳定地存在对象头里供后续读取。既然两者都要抢这同一块 64 位内存,JVM 只能二选一:调用 hashCode 后撤销偏向锁、退到无锁态,之后直接从轻量级锁开始。

Object obj = new Object();
obj.hashCode();  // hashCode 写入 Mark Word,偏向锁从此与它绝缘
// 此后的 synchronized(obj) 直接从轻量级锁开始,跳过偏向锁

批量重偏向(Bulk Rebias)

如果同一个类的多个对象频繁发生偏向锁撤销(一个线程持有,另一个线程来抢),JVM 不逐个撤销——当撤销次数达到 20 次(BiasedLockingBulkRebiasThreshold),对该类的所有对象批量更新偏向目标(epoch + 1),避免 STW 逐个撤销。

当撤销次数达到 40 次(BiasedLockingBulkRevokeThreshold),JVM 批量撤销该类的所有偏向锁——之后该类所有对象都不再使用偏向锁。

JIT 的锁优化

锁消除:逃逸分析证明对象只在线程内部使用,JIT 直接去掉 synchronized

public String concat(String a, String b) {
    StringBuffer sb = new StringBuffer(); // sb 只在本方法用,不逃逸
    sb.append(a);                         // append() 里的 synchronized 被 JIT 消除
    sb.append(b);
    return sb.toString();
}

锁粗化:连续对同一对象加锁,JIT 合并为一次。

// 优化前
synchronized (lock) { a(); }
synchronized (lock) { b(); }
synchronized (lock) { c(); }

// JIT 优化后
synchronized (lock) { a(); b(); c(); }

想一想:锁消除和锁粗化分别是解决什么「过度加锁」? 锁消除解决的是「根本不需要加锁」——逃逸分析证明对象只在单线程内用,锁是白白加的,直接删掉;锁粗化解决的是「同一把锁加得太碎」——连续多次对同一对象加锁,每次都要走一遍加锁/解锁开销,不如合并成一次大锁。两者都是 JIT 在字节码层面对程序员「无脑加锁」的补偿优化。

总结

锁状态Mark Word 存什么开销适用场景
偏向锁ThreadID (54bit)一次 CAS + STW 撤销单线程反复访问
轻量级锁Lock Record 指针 (62bit)CAS 自旋两个线程交替获取
重量级锁Monitor 对象指针 (62bit)OS 互斥量 + 内核态切换真实竞争

synchronized 不是重锁——JDK 6 之后的锁升级让它在无竞争场景下开销接近零,加上 JIT 的锁消除和锁粗化,绝大多数场景不需要手动用 ReentrantLock 替代。只有需要限时等锁(tryLock)、可中断等锁(lockInterruptibly)、公平锁这些 synchronized 不支持的特性时,才需要 ReentrantLock

章末提问

  1. 为什么 JDK 15 默认禁用偏向锁、JDK 18 彻底移除? 结论:因为现代 CPU 的 CAS 已经足够快,偏向锁的 STW 撤销成本大于它省下的那点 CAS 开销。偏向锁撤销时必须 STW 暂停所有线程,而如今无竞争场景省下的开销微乎其微,所以得不偿失。

  2. synchronized 的锁升级是怎样的状态机?会降级吗? 结论:无锁 → 偏向锁 → 轻量级锁 → 重量级锁的四级单向状态机,不会降级。因为膨胀后再降级要撤销 Monitor、改回 CAS 路径,来回切换的开销比一直保持重量级更大。

  3. 轻量级锁和重量级锁的本质区别是什么? 结论:轻量级锁全程用户态 CAS 自旋、不进内核;重量级锁要膨胀成堆上的 ObjectMonitor、竞争线程用 OS 互斥量阻塞。因为轻量级锁赌竞争短暂、靠自旋等锁,重量级锁处理的是真正长时间的竞争。

  4. 为什么调用了 hashCode() 的对象就再也用不了偏向锁? 结论:因为偏向态 Mark Word 的 54 bit 全被线程 ID 占满,没位置放 31 bit 的 hashCode。两者抢同一块 64 位内存,只能二选一,所以调用 hashCode 后必须撤销偏向锁退回无锁态。

  5. 什么时候该用 ReentrantLock 而不是 synchronized? 结论:需要 tryLock 限时等锁、lockInterruptibly 可中断、或公平锁时。因为这三个特性 synchronized 都不支持,其余场景 synchronized 经锁升级 + JIT 锁消除/粗化后开销已接近零,不必手动换锁。


Share this post on:

Previous Post
Java线上排查——从top到Arthas的完整工具链
Next Post
happens-before规则详解:JMM的8条核心约束