OpenJDK9 Hotspot : synchronized 浅析

xingpingz

前言

网上各路大神总结过各种关于 hotspot jvm synchronized 内部实现,看别人的文章总觉得不过瘾,所以有了这篇文章,尝试再扒一次 synchronized 的“底裤”

数据结构

在分析源代码之前需要了解相关概念,比如 oop, oopDesc, markOop 等,参考网络上各种解说或者之前系列文章,这里重点介绍一下 markOop,ObjectWaiter,ObjectMonitor .etc

markOop

每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,回顾一下 oopDesc 的类定义(内存布局)

class oopDesc {
private:
    volatile markOop _mark;
}

_mark 被声明在 oopDesc 类的顶部,所以这个 _mark 可以认为是一个 头部(就像 TCP/IP 数据包头部),我们知道"头部"一般保存着一些重要的状态和标志信息,在 markOop.hpp 文件头部有一大段注释说明 markOop 内存布局

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

这里只列出 32 位机器上 markOop 的内存布局,同样的 32 bit 在不同的 object(normal, biased)以及不同的 CMS 垃圾搜集状态下有不同的解释,这种紧凑的内存复用技术在 C/C++ 系统编程中随处可见

对于 normal object,32 bit 位分为 4 个字段,其中和 synchronized 相关的是 biased_lock 和 lock

  • hash,对象的 hash 值

  • age,对象的年龄,分代 GC 相关

  • biased_lock,偏向锁标志

  • lock,对象锁标志

  1. 占两比特,用于描述 3 种状态 locked, unlocked, monitor

//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time

对于 biased boject,biased_lock 比特位被设置,如果对象被偏向锁定,拥有该偏向锁的线程指针被保存在 markOop 的高位

//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased

ObjectWaiter

如果一个线程在等待 object monitor(对象监视器),虚拟机会创建一个 ObjectWaiter 对象,并通过 _next 和 _prev 指针将 ObjectWaiter 挂载到 object monitor 中的等待队列中

class ObjectWaiter : public StackObject {
public:
    ObjectWaiter * volatile _next;
    ObjectWaiter * volatile _prev;
    Thread* _thread
    ...
}

ObjectMonitor

ObjectMonitor 类是对 对象监视器 的封装,由于比较重要(关键),objectMonitor.hpp 文件中对它进行了大段注释

// The ObjectMonitor class implements the heavyweight version of a
// JavaMonitor. The lightweight BasicLock/stack lock version has been
// inflated into an ObjectMonitor. This inflation is typically due to
// contention or use of Object.wait().

从注释可以看出 ObjectMonitor 是 JavaMonitor(对象锁)的一个重量级实现,而偏向锁和 stack lock(?)是另一种轻量级实现,当调用 Object.wait() 方法时,轻量级 JavaMonitor 会膨胀(提升)成重量级实现

关键字段

_owner

当前拥有该 ObjectMonitor 的线程

_EntryList

由 ObjectWaiter 组成的双向链表,JVM 会从该链表中取出一个 ObjectWaiter 并唤醒对应的 JavaThread

_cxq

JVM 为每个尝试进入 synchronized 代码段的 JavaThread 创建一个 ObjectWaiter 并添加到 _cxq 队列中

_WaitSet

JVM 为每个调用 Object.wait() 方法的线程创建一个 ObjectWaiter 并添加到 _WaitSet 队列中

synchronized 实现

在进入 synchronized 代码块或方法时,javac 会插入一条 monitorenter 字节码指令,退出时插入一条 monitorexit 指令,我们还是以 Zero 解释器为例来看看 monitorenter/monitorexit 指令是如何实现的,关于 Zero 解释器相关概念可以参考之前的文章

monitorenter

在 bytecodeInterpreter.cpp 中能够找到 monitorenter 对应的 case,大概流程如下:

  1. 获取方法隐含的 this 参数,即 oop

  2. 获取对象头部 markOop(参考上文),判断是否有偏向标志(has_bias_pattern),如果没有转到 4

  3. 偏向锁相关的处理逻辑

  4. 尝试使用轻量级锁,这里使用了 CAW(compare and swap,比较和交换)原语来保证线程对 oop 中 markOop
    字段的独占写入,成功写入的线程立即返回(接着运行),失败的线程则调用 InterpreterRuntime::monitorenter
    方法(重量级锁)

至此可以看出加锁的顺序:偏向锁 -> 轻量级锁 -> 重量级锁

CASE(_monitorenter): {
    oop lockee = STACK_OBJECT(-1);
    ...
    if (entry != NULL) {
        entry->set_obj(lockee);
        ...
        markOop mark = lockee->mark();
        intptr_t hash = (intptr_t) markOopDesc::no_hash;
        if (mark->has_bias_pattern()) {
            // 尝试使用偏向锁...
        }

        // 尝试使用轻量级锁
        // traditional lightweight locking
        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) {
              // Is it simple recursive case?
              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);
              }
            }
        }
    } else {
        istate->set_msg(more_monitors);
        UPDATE_PC_AND_RETURN(0);
    }
}

我们先把偏向锁相关的代码放一遍,接着看 InterpreterRuntime::monitorenter 方法,为了使代码更加清晰,我们忽略掉断言和条件编译,

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread,   
        BasicObjectLock* elem))
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
IRT_END

看来 JVM 还是不死心,这里又有两个分支 fast_enter 和 slow_enter,由于一路上我们都是挑着最慢的路径走,这回也不例外,接着扒 slow_enter 方法

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();

  if (mark->is_neutral()) {
    lock->set_displaced_header(mark);
    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())) {
    lock->set_displaced_header(NULL);
    return;
  }

  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD,
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}

再次通过 cmpxchg 尝试轻量级锁,否则调用 ObjectSynchronizer:: inflate 方法膨胀成重量级锁(ObjectMonitor)并调用其 enter 方法

ObjectMonitor::enter

ObjectMonitor 对象有一个 _owner 字段表明当前哪个线程持有 ObjectMonitor,enter 方法首先通过 cmpxchg 尝试将 _owner 原子性设置成当前线程,如果成功就直接返回,这样可以避免进行内核线程的上下文切换

总结

阅读 4.2k

signal
随笔

博学,审问,慎思,明辨,力行

119 声望
62 粉丝
0 条评论
你知道吗?

博学,审问,慎思,明辨,力行

119 声望
62 粉丝
宣传栏