资讯专栏INFORMATION COLUMN

OpenJDK9 Hotspot : synchronized 浅析

Amio / 1836人阅读

摘要:前言网上各路大神总结过各种关于内部实现,看别人的文章总觉得不过瘾,所以有了这篇文章,尝试再扒一次的底裤数据结构在分析源代码之前需要了解相关概念,比如等,参考网络上各种解说或者之前系列文章,这里重点介绍一下,,每个在内部都有一个的对象与之对应

前言

网上各路大神总结过各种关于 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,对象锁标志

占两比特,用于描述 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,大概流程如下:

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

获取对象头部 markOop(参考上文),判断是否有偏向标志(has_bias_pattern),如果没有转到 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 原子性设置成当前线程,如果成功就直接返回,这样可以避免进行内核线程的上下文切换

总结

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66732.html

相关文章

  • 线程间的同步与通信(3)——浅析synchronized的实现原理

    摘要:由此可见,自旋锁和各有优劣,他们分别适用于竞争不多和竞争激烈的场景中。每一个试图进入同步代码块的线程都会被封装成对象,它们或在对象的中,或在中,等待成为对象的成为的对象即获取了监视器锁。 前言 系列文章目录 前面两篇文章我们介绍了synchronized同步代码块以及wait和notify机制,大致知道了这些关键字和方法是干什么的,以及怎么用。 但是,知其然,并不知其所以然。 例如...

    keithxiaoy 评论0 收藏0
  • OpenJDK9 Hotspot : Object wait/notify 方法浅析

    摘要:前言方法是早期提供的一种基于的线程同步方法,本文先介绍相关的数据结构类,然后从方法的内部实现入手,简单分析相关的原理和实现类用于实现的定待和唤醒,不同平台操作系统平台对应的定义在文件类的分配和释放使用了对象缓存,静态字段用于缓存当前 前言 Object wait/notify 方法是早期 JVM 提供的一种基于 Object Monitor 的线程同步方法,本文先介绍相关的数据结构(类...

    fancyLuo 评论0 收藏0
  • OpenJDK9 Hotspot : Thread

    摘要:前言本文从类的方法的内部实现入手,分析多线程相关的数据结构类和原理类方法类的方法用于启动线程,方法内部调用了方法在源代码中搜索,可以看到对应函数在源代码中搜索函数核心代码计算线程堆栈大小创建对象初始化启动线程在创建时传入了一个函数指针, 前言 本文从 Java Thread 类的 start 方法的内部实现入手,分析 Hotspot JVM 多线程相关的数据结构(类)和原理 Threa...

    roundstones 评论0 收藏0
  • OpenJDK9 Hotspot:Zero 解释器 初探

    摘要:准备工作假设源代码目录为编译时启用了解释器参考编译和调试调用栈先在函数参考虚拟机入口中设断点,然后在的方法中设置断点通过宏获取当前,然后创建第个栈帧,然后进入解释执行字节码 准备工作 假设 openjdk 源代码目录为 jdk9dev 编译 openjdk 时启用了 zero 解释器(参考 OpenJDK9 Hotspot Mac OSX 编译和调试) 调用栈 先在 JavaMai...

    zhangqh 评论0 收藏0
  • OpenJDK9 Hotspot Mac OSX 编译和调试

    摘要:占用率太高,还出各种奇怪问题,转投调试安装下载源代码漫长等待,中间无数次中断安装安装可选如果要使用解释器,需要安装设置调试级别,设成可以提供更多的调试信息设置路径 Intellij CLion CPU 占用率太高,还出各种奇怪问题,转投 Xcode 调试 hotspot 安装 hg # brew install hg 下载 open jdk 9 源代码 # hg clone http...

    zhichangterry 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<