一.引言
“操作系统的线程状态和java的线程状态有什么关系?”这是校招时被问到的一个问题。当时只顾着看博文、面经等零散的资料,没有形成系统的知识体系,一时语塞,答的不是很对。在网上也没找到足够细致地讲解博文,于是整理出了这篇内容。
Issue:有的同学可能会问了,对象一开始不是无锁状态吗,为什么上述偏向锁逻辑没有判断 无锁状态的锁对象 (001)?
只有匿名偏向的对象才能进入偏向锁模式。偏向锁是延时初始化的,默认是4000ms。初始化后会将所有加载的Klass的prototype header修改为匿名偏向样式。当创建一个对象时,会通过Klass的prototype_header来初始化该对象的对象头。简单的说,偏向锁初始化结束后,后续所有对象的对象头都为 匿名偏向 样式,在此之前创建的对象则为 无锁状态 。而对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁。这也是为什么JVM启动前4秒对象会直接进入到轻量级锁的原因。
为什么需要延迟初始化?
JVM启动时必不可免会有大量sync的操作,而偏向锁并不是都有利。如果开启了偏向锁,会发生大量锁撤销和锁升级操作,大大降低JVM启动效率。
因此,我们可以明确地说,只有锁对象处于 匿名偏向 状态,线程才能拿到到我们通常意义上的偏向锁。而处于无锁状态的锁对象,只能进入到轻量级锁状态。
2.3.2 偏向锁的撤销
偏向锁的 撤销 (revoke)是一个很特殊的操作,为了执行撤销操作,需要等待 全局安全点 ,此时所有的工作线程都停止了执行。偏向锁的撤销操作并不是将对象恢复到无锁可偏向的状态,而是在偏向锁的获取过程中,发现竞争时,直接将一个被偏向的对象 升级到 被加了轻量级锁的状态。这个操作的具体完成方式如下:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) ... Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); // 开启了偏向锁 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); } ...
如果开启了JVM偏向锁,则会进入到ObjectSynchronizer::fast_enter 方法中。
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"); //批量撤销,底层调用bulk_revoke_or_rebias_at_safepoint BiasedLocking::revoke_at_safepoint(obj); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } slow_enter (obj, lock, THREAD) ;}
主要看BiasedLocking::revoke_and_rebias 方法。这个方法的主要作用像它的方法名:撤销或者重偏向。第一个参数封装了锁对象和当前线程,第二个参数代表是否允许重偏向,这里是true。
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) { assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint"); markOop mark = obj->mark(); //获取锁对象的对象头 if (mark->is_biased_anonymously() && !attempt_rebias) { // 如果锁对象为匿名偏向状态且不允许重偏向下,进入该分支。在一个非全局安全点进行偏向锁撤销 markOop biased_value = mark; // 创建一个匿名偏向的markword markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); // 通过cas重新设置偏向锁状态 markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark); if (res_mark == biased_value) {// 如果CAS成功,返回偏向锁撤销状态 return BIAS_REVOKED; } } else if (mark->has_bias_pattern()) { // 锁为偏向模式(101)会走到这里 Klass* k = obj->klass(); markOop prototype_header = k->prototype_header(); // 如果对应class关闭了偏向模式 if (!prototype_header->has_bias_pattern()) { markOop biased_value = mark; // CAS更新对象头markword为非偏向锁 markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark); assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked"); return BIAS_REVOKED; // 返回偏向锁撤销状态 } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { // 如果epoch过期,则进入当前分支 if (attempt_rebias) { // 如果允许重偏 assert(THREAD->is_Java_thread(), ""); markOop biased_value = mark; markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch()); // 通过CAS操作, 将本线程的 ThreadID 、时间戳、分代年龄尝试写入对象头中 markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark); if (res_mark == biased_value) { //CAS成功,则返回撤销和重新偏向状态 return BIAS_REVOKED_AND_REBIASED; } } else { // 如果不允许尝试获取偏向锁,进入该分支取消偏向 // 通过CAS操作更新分代年龄 markOop biased_value = mark; markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark); if (res_mark == biased_value) { //如果CAS操作成功,返回偏向锁撤销状态 return BIAS_REVOKED; } } } } //执行到这里有以下两种情况: //1.对象不是偏向模式 //2.上面的cas操作失败 HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias); if (heuristics == HR_NOT_BIASED) { // 非偏向从这出去 // 轻量级锁、重量级锁 return NOT_BIASED; } else if (heuristics == HR_SINGLE_REVOKE) { // 撤销单个线程 // Mark,最常见的执行分支 // Mark,最常见的执行分支 // Mark,最常见的执行分支 Klass *k = obj->klass(); markOop prototype_header = k->prototype_header(); if (mark->biased_locker() == THREAD && prototype_header->bias_epoch() == mark->bias_epoch()) { // 偏向当前线程且不过期 // 这里撤销的是偏向当前线程的锁,调用Object#hashcode方法时也会走到这一步 // 因为只要遍历当前线程的栈就能拿到lock record了,所以不需要等到safe point再撤销。 ResourceMark rm; if (TraceBiasedLocking) { tty->print_cr("Revoking bias by walking my own stack:"); } BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD); ((JavaThread*) THREAD)->set_cached_monitor_info(NULL); assert(cond == BIAS_REVOKED, "why not?"); return cond; } else { // 下面代码最终会在safepoint调用revoke_bias方法撤销偏向 VM_RevokeBias revoke(&obj, (JavaThread*) THREAD); VMThread::execute(&revoke); return revoke.status_code(); } } assert((heuristics == HR_BULK_REVOKE) || (heuristics == HR_BULK_REBIAS), "?"); //批量撤销、批量重偏向的逻辑 VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD, (heuristics == HR_BULK_REBIAS), attempt_rebias); VMThread::execute(&bulk_revoke); return bulk_revoke.status_code();}
这块代码注释写的算是比较清楚,只简单介绍下最常见的情况:锁已经偏向线程A,此时线程B尝试获取锁。这种情况下会走到Mark标记的分支。如果需要撤销的是当前线程,只要遍历当前线程的栈就能拿到lock record,可以直接调用 revoke_bias ,不需要等到safe point再撤销。在调用Object#hashcode时,也会走到该分支将为偏向锁的锁对象直接恢复为无锁状态。若不是当前线程,会被push到VM Thread中等到 safepoint 的时候再执行。
VMThread内部维护了一个VMOperationQueue类型的队列,用于保存内部提交的VM线程操作VM_operation。GC、偏向锁的撤销等操作都是在这里被执行。
撤销调用的 revoke_bias 方法的代码就不贴了。大致逻辑是:
步骤 1 、查看偏向的线程是否存活,如果已经死亡,则直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。
步骤 2 、偏向的线程是否还在同步块中,如果不在,则撤销偏向锁。如果在同步块中,执行步骤3。这里 是否在同步块的判断 基于上文提到的偏向锁的重入计数方式:在偏向锁的获取中,每次进入同步块的时候都会在栈中找到第一个可用(即栈中最高的)的 Lock Record ,将其obj字段指向锁对象。每次解锁的时候都会把最低的 Lock Record 移除掉,所以可以通过遍历线程栈中的 Lock Record 来判断是否还在同步块中。轻量级锁的重入也是基于 Lock Record 的计数来判断。
步骤 3 、升级为轻量级锁。将偏向线程所有相关 Lock Record 的 Displaced Mark Word 设置为null,再将最高位的 Lock Record 的 Displaced Mark Word 设置为无锁状态,然后将对象头指向最高位的 Lock Record 。这里没有用到CAS指令,因为是在 safepoint ,可以直接升级成轻量级锁。
2.3.3 偏向锁的释放
偏向锁的释放可参考bytecodeInterpreter.cpp#1923,这里也不贴了。偏向锁的释放只要将对应 Lock Record 释放就好了,但这里的释放并不会将mark word里面的thread ID去掉,这样做是为了下一次更方便的加锁。而轻量级锁则需要将 Displaced Mark Word 替换到对象头的mark word中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit 方法中。
2.3.4 批量重偏向与撤销
从上节偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到 safe point 时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。因此,JVM中增加了一种批量重偏向/撤销的机制以减少锁撤销的开销,而mark word中的epoch也是在这里被大量应用,这里不展开说明。但无论怎么优化,偏向锁的撤销仍有一定不可避免的成本。如果业务场景存在大量多线程竞争,那偏向锁的存在不仅不能提高性能,而且会导致性能下降( 偏向锁并不都有利,jdk15默认不开启 )。
2.4 轻量级锁
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁使用的操作系统互斥量带来的开销,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
2.4.1 进入轻量级锁
轻量级锁在上文或多或少已经涉及到,其获取流程入口为bytecodeInterpreter.cpp#1816。前大半部分都是偏向锁逻辑,还有一部分为轻量级锁逻辑。在偏向锁逻辑中,cas失败会执行到InterpreterRuntime::monitorenter 。在轻量级锁逻辑中,如果当前线程不是轻量级锁的重入,也会执行到InterpreterRuntime::monitorenter 。我们再看看InterpreterRuntime::monitorenter 方法:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) ... Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); 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
fast_enter 的流程在偏向锁的撤销小节中已经分析过,主要逻辑为 revoke_and_rebias :如果当前是偏向模式且偏向的线程还在使用锁,会将锁的 mark word 改为轻量级锁的状态,并将偏向的线程栈中的 Lock Record 修改为轻量级锁对应的形式(此时Lock Record是无锁状态),且返回值不是 BIAS_REVOKED_AND_REBIASED ,会继续执行 slow_enter 。
我们直接看 slow_enter 的流程:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { // 步骤1 markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); // 步骤2 // 如果为无锁状态 if (mark->is_neutral()) { // 步骤3 // 设置mark word到栈 lock->set_displaced_header(mark); // CAS更新指向栈中Lock Record的指针 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } // Fall through to inflate() ... cas失败走下面锁膨胀方法 } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { // 步骤4 // 为轻量级锁且owner为当前线程 assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); // 设置Displaced Mark Word为null,重入计数用 lock->set_displaced_header(NULL); return; } // 步骤5 // 走到这一步说明已经是存在多个线程竞争锁了,需要膨胀或已经是重量级锁 lock->set_displaced_header(markOopDesc::unused_mark()); // 进入、膨胀到重量级锁的入口 // 膨胀后再调用monitor的enter方法竞争锁 ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);}
步骤 1 、 markOop mark = obj->mark() 方法获取对象的markOop数据mark;
步骤 2 、 mark->is_neutral() 方法判断mark是否为无锁状态,标识位 001 ;
步骤 3 、如果mark处于无锁状态,把mark保存到BasicLock对象(Lock Record的属性)的displaced_header字段;
步骤 3.1 、通过CAS尝试将Mark Word更新为指向BasicLock对象的指针,如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤4;
步骤 4 、如果是重入,则设置Displaced Mark Word为null。
步骤 5 、到这说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁;
结合上文偏向锁的流程,可以整理得到如下的流程图:
ObjectMonitor 竞争失败的线程,通过自旋执行 ObjectMonitor::EnterI 方法等待锁的释放,EnterI方法的部分逻辑实现如下:
void ATTR ObjectMonitor::EnterI (TRAPS) { // 尝试自旋 if (TrySpin (Self) > 0) { ... return ; } ... // 将线程封装成node节点中 ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; // 将node节点插入到_cxq队列的头部,cxq是一个单向链表 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; // CAS失败的话 再尝试获得锁,这样可以降低插入到_cxq队列的频率 if (TryLock (Self) > 0) { ... return ; } } ...}
EnterI大致原理:一个 ObjectMonitor 对象包括两个同步队列( _cxq 和 _EntryList ) ,以及一个等待队列 _WaitSet 。cxq、EntryList 、WaitSet都是由ObjectWaiter构成的链表结构。其中, _cxq 为单向链表, _EntryList 为双向链表。
这里有几个点可以注意一下:
- HotSpot中,只用到了 模板解释器 ,并没有用到字节码解释器, monitorenter 的实际入口位于templateTable_x86_64.cpp#3667。本文的分析是基于字节码解释器的,因此部分结论不能作为实际执行情况。本章的内容只能作为Synchronized锁升级原理、各类锁的适用场景的一种 窥探 。
- 再次强调,无锁状态只能升级为轻量级锁, 匿名偏向状态 才能进入到偏向锁。
- 偏向锁 并不都有利, 其适用于 单个线程重入 的场景,原因为:偏向锁的撤销需要进入 safepoint ,开销较大。需要进入 safepoint 是由于,偏向锁的撤销需要对锁对象的 lock record 进行操作,而 lock record 要到每个线程的栈帧中遍历寻找。在非safepoint,栈帧是动态的,会引入更多的问题。目前看来,偏向锁存在的价值是为历史遗留的Collection类如Vector和HashTable等做优化,迟早药丸。Java 15中默认不开启。
- 执行Object类的 hashcode 方法,偏向锁撤销并且锁会膨胀为轻量级锁或者重量锁。执行Object类的 wait/notify/notifyall 方法,偏向锁撤销并膨胀成重量级锁。
- 轻量级锁适用于 两个线程的交替执行 场景:线程A进入轻量级锁,退出同步代码块并释放锁,会将锁对象恢复为无锁状态;线程B再进入锁,发现为无锁状态,会cas尝试获取该锁对象的轻量级锁。如果有竞争,则直接膨胀为重量级锁,没有自旋操作,详情看10。
- 唤醒策略依赖于 QMode 。重量级锁获取失败后,线程会加入cxq队列。当线程释放锁时,会从cxq或EntryList中挑选一个线程唤醒。线程获得锁后调用 Object#wait 方法,则会将线程加入到WaitSet中。当被 Object#notify 唤醒后,会将线程从WaitSet移动到cxq或EntryList中去。
- 重量级锁,会将线程放进等待队列,等待操作系统调度。而偏向锁和轻量级锁,未交由操作系统调度,依然处于用户态,只是采用CAS无锁竞争的方式获取锁。CAS通过Unsafe类中compareAndSwap方法,jni调用C++方法,通过汇编指令锁住cpu中的北桥信号。
- 许多文章声称一个对象关联到一个monitor,这个说法不够准确。如果对象已经是重量级锁了,对象头的确指向了一个 monitor 。但对于正在膨胀的锁,会先从 线程私有 的 monitor 集合 omFreeList 中分配对象。如果 omFreeList 中已经没有 monitor 对象,再从 JVM全局 的 gFreeList 中分配一批 monitor 到 omFreeList 中。
- 在编译期间还有 锁消除 和 锁粗化 这两步锁优化操作,本章没做介绍。
- 字节码实现中没有体现轻量级锁自旋逻辑。这可能是模板解释器中的实现,或者是jvm在不同平台、不同jvm版本的不同实现。但本文分析的字节码链路中没有发现该逻辑,倒是发现了 重量级锁会自适应自旋竞争锁 。因此个人对轻量级锁自适应自旋的说法存疑,至少hotspot jdk8u字节码实现中没有这个逻辑。但两者都是在用户态进行自适应自旋,以尽可能减少同步操作带来的开销,没有太多本质上的区别,并不需要特别关心。
三、线程的实现与状态转换
3.1 线程的实现
(1)内核线程实现
内核线程(Kernel-Level Thread,KLT):由 内核 来完成线程切换,内核通过 调度器 对线程进行调度,并负责将线程的任务 映射 到各个处理器上。程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口—— 轻量级进程 (Light Weight Process,LWP),也就是通常意义上的线程。
优点:每个LWP都是独立的调度单元。一个LWP被阻塞,不影响其他LWP。
缺点:基于KLT,耗资源。线程的创建、析构、同步都需要进行系统调用,频繁的用户态、内核态切换。
(3) 混合实现 混合模式下, 即存在用户线程,也存在轻量级进程 。用户线程的创建、切换、析构等操作依然廉价,可以支持大规模的用户线程并发,且可以使用内核线程提供的线程调度功能及处理器映射。
既然聊到了 wait 和 notify ,那顺便也看下 join 、 sleep 和 park 。
打开 Thread.join() 的源码:
public final synchronized void join(long millis) throws InterruptedException { ... if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } }}
join 的本质仍然是 wait() 方法。在使用 join 时,JVM会帮我们隐式调用 notify ,因此我们不需要主动notify唤醒主线程。而 sleep() 方法最终是调用 SleepEvent 对象的park方法:
int os::sleep(Thread* thread, jlong millis, bool interruptible) { //获取thread中的_SleepEvent对象 ParkEvent * const slp = thread->_SleepEvent ; ... //如果是允许被打断 if (interruptible) { //记录下当前时间戳,这是时间比较的基准 jlong prevtime = javaTimeNanos(); for (;;) { //检查打断标记,如果打断标记为true,则直接返回 if (os::is_interrupted(thread, true)) { return OS_INTRPT; } //线程被唤醒后的当前时间戳 jlong newtime = javaTimeNanos(); //睡眠毫秒数减去当前已经经过的毫秒数 millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC; //如果小于0,那么说明已经睡眠了足够多的时间,直接返回 if (millis <= 0) { return OS_OK; } //更新基准时间 prevtime = newtime; //调用_SleepEvent对象的park方法,阻塞线程 slp->park(millis); } } else { //如果不能打断,除了不再返回OS_INTRPT以外,逻辑是完全相同的 for (;;) { ... slp->park(millis); ... } return OS_OK ; }}
Thread.sleep 在jvm层面上是调用thread中 SleepEvent 对象的 park() 方法实现阻塞线程,在此过程中会通过判断时间戳来决定线程的睡眠时间是否达到了指定的毫秒。看到这里,对于 sleep 和 wait 的区别应该会有更深入的理解。
park 、 unpark 方法也与同步语义无关。每个线程都与一个许可(permit)关联。 unpark 函数为线程提供permit,线程调用 park 函数则等待并消耗permit。park和unpark方法具体实现比较复杂,这里不展开。到此为止,我们可以整理出如下的线程状态转换图。
3.3 本章小节
Java 将OS经典五种状态中的ready和running,统一为 RUNNABLE。将WAITING(即不可能得到 CPU 运行机会的状态)细分为了 BLOCKED、WAITING、TIMED_WAITING。本章的内容较为简短,因为部分的内容已囊括在第一章中。
这里提个会使人困惑的问题:使用socket时,调用accept(),read() 等阻塞方法时,线程处于什么状态?
答 案是java线程处于RUNNABLE状态,OS线程处于WAITING状态。因为在jvm层面,等待cpu时间片和等待io资源是等价的。
这里有几个点可以注意一下:
- JVM线程状态不代表内核线程状态;
- BLOCKED的线程一定处于entryList或cxq中,而处于WAITING和TIMED WAITING的线程,可能是由于执行了sleep或park进入该状态,不一定在waitSet中。也就是说,处于BLOCKED状态的线程一定是与同步相关。由这可延伸出,调用 jdk 的 lock并获取不到锁的线程,进入的是 WAITING 或 TIMED_WAITING 状态,而不是BLOCKED状态。