OpenJDK9 Hotspot : synchronized 浅析

前言

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

数据结构

在分析源代码以前须要了解相关概念,好比 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++ 系统编程中随处可见 app

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

  • hash,对象的 hash 值oop

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

  • biased_lock,偏向锁标志this

  • 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 原子性设置成当前线程,若是成功就直接返回,这样能够避免进行内核线程的上下文切换

总结

相关文章
相关标签/搜索