Java锁与线程的那些“不可描述”的事儿

atomic[64]_cmpxchg之坑 atomic64_cmpxchg、atomic_cmpxchg是宏定义,第一个参数使用强制类型转换,移植代码到64位系统时容易入坑。 阅读详情

一.引言

“操作系统的线程状态和java的线程状态有什么关系?”这是校招时被问到的一个问题。当时只顾着看博文、面经等零散的资料,没有形成系统的知识体系,一时语塞,答的不是很对。在网上也没找到足够细致地讲解博文,于是整理出了这篇内容。

Java的线程状态牵扯到了同步语义,要探讨Java的线程状态的,必不可免要回顾其锁机制。因此本文的主要分为两大块:一是Synchronized源码粗析,分析了各类锁的进入、释放、升级过程,并大致说明了monitor原理;二是介绍了线程的实现方式和Java线程状态转换的部分细节。

二. Synchronized锁

Java采用synchronized关键字、以互斥同步的方式的解决线程安全问题,那么什么是线程安全呢?这里引用《Java并发编程实战》作者Brian Goetz给出的定义:

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。—— Brian Goetz

2.1 Synchronized的使用

先写过个demo,大致过一下synchronized的使用,包含同步代码块、实例方法和静态方法。

public synchronized void test1(){
  }

  public void test2(){
    synchronized(new Test()){
    }
  }

  public static synchronized void test3(){
  }

反编译可查看字节码:

public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED    // here

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/easy/helloworld/Test
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: dup
         8: astore_1
         9: monitorenter                   // here
        10: aload_1
        11: monitorexit                    // here
        12: goto          20
        15: astore_2
        16: aload_1
        17: monitorexit                    // here
        18: aload_2
        19: athrow
        20: return

  public static synchronized void test3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED   // here

可以观察到:

  • 同步代码:通过moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
  • 实例方法:隐式调用moniterenter、moniterexit
  • 静态方法:隐式调用moniterenter、moniterexit

2.2 Moniterenter、Moniterexit

monitorenter和monitorexit这两个jvm指令,主要是基于 Mark Word 和 Object monitor 来实现的。

在 JVM 中,对象在内存中分为三块区域:

  • 对象头:由 Mark Word 和 Klass Point 构成。
    • 1. Mark Word(标记字段): 用于存储对象自身的运行时数据,例如存储对象的HashCode,分代年龄、锁标志位等信息,是synchronized实现轻量级锁和偏向锁的关键。64位JVM的Mark Word组成如下:
    • 2. Klass Point(类型指针): 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据:这部分主要是存放类的数据信息,父类的信息。
  • 字节对齐:为了内存的IO性能,JVM要求对象起始地址必须是8字节的整数倍。对于不对齐的对象,需要填充数据进行对齐。

在JDK 1.6之前, synchronized 只有传统的锁机制,直接关联到 monitor 对象,存在性能上的瓶颈。在JDK 1.6后,为了提高锁的获取与释放效率,JVM引入了两种锁机制:偏向锁和轻量级锁。它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。这几种锁的实现和转换正是依靠对象头中的 Mark Word 。

本章内容近万字,时间不充裕的同学可以直接看 本章小节 。

2.3 偏向锁

引入偏向锁的目的:在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令。但在多线程竞争时,需要进行偏向锁撤销步骤,因此其撤销的开销必须小于节省下来的CAS开销,否则偏向锁并不能带来收益。JDK 1.6中默认开启偏向锁,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

2.3.1 进入偏向锁

关于HotSpot虚拟机中获取锁的入口,网上主要有两种看法:一为interpreterRuntime.cpp#monitorenter#1608;二为bytecodeInterpreter.cpp#1816。在HotSpot的中,有两处地方对 monitorenter 指令进行解析:一个是bytecodeInterpreter.cpp#1816 ,另一个在templateTable_x86_64.cpp#3667。其中, bytecodeInterpreter 是JVM中的字节码解释器, templateInterpreter 为模板解释器。HotSpot对运行效率有着极其执着的追求,显然会倾向于用模板解释器来实现。R大的读书笔记中有说明,HotSpot中只用到了模板解释器,并没有用到字节码解释器。因此,本文认为 montorenter 的解析入口为templateTable_x86_64.cpp#3667。

但模板解释器 templateInterpreter 都是汇编代码,不易读,且实现逻辑与字节码解释器 bytecodeInterpreter 大体一致。因此本文的源码都以 bytecodeInterpreter 来说明,借此窥探 synchronized 的实现原理。在看代码之前,先介绍几个在偏向锁中会被大量应用的概念,以便后续理解:

prototype_header :JVM中的每个类有一个类似 mark word 的 prototype_header ,用来标记该class的 epoch 和偏向开关等信息。

匿名偏向状态 :锁对象mark word标志位为101,且存储的 Thread ID 为空时的状态(即锁对象为偏向锁,且没有线程偏向于这个锁对象)。

Atomic::cmpxchg_ptr :CAS函数。这个方法有三个参数,依次为 exchange_value 、 dest 、 compare_value 。如果dest的值为 compare_value 则更新为 exchange_value ,并返回 compare_value 。否则,不更新并返回 实际原值 。

接下来开始源码实现分析,HotSpot中偏向锁的具体实现可参考bytecodeInterpreter.cpp#1816,代码如下:

CASE(_monitorenter): {
  //锁对象
  oop lockee = STACK_OBJECT(-1);
  // derefing's lockee ought to provoke implicit null check
  CHECK_NULL(lockee);
  // 步骤1
  // 在栈中找到第一个空闲的Lock Record
  // 会找到栈中最高的
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  BasicObjectLock* entry = NULL;
  while (most_recent != limit ) {
    if (most_recent->obj() == NULL) entry = most_recent;
    else if (most_recent->obj() == lockee) break;
    most_recent++;
  }
  // entry不为null,代表还有空闲的Lock Record
  if (entry != NULL) {
    // 将Lock Record的obj指针指向锁对象
    entry->set_obj(lockee);
    int success = false;
    uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
    // markoop即对象头的mark word
    markOop mark = lockee->mark();
    intptr_t hash = (intptr_t) markOopDesc::no_hash;
    // 步骤2
    // implies UseBiasedLocking
    // 如果为偏向模式,即判断标识位是否为101
    if (mark->has_bias_pattern()) {
      ...
      // 一顿操作
      anticipated_bias_locking_value =
        (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
        ~((uintptr_t) markOopDesc::age_mask_in_place);
      // 步骤3
      if  (anticipated_bias_locking_value == 0) {
        // already biased towards this thread, nothing to do
        // 偏向的是自己,啥都不做
        if (PrintBiasedLockingStatistics) {
          (* BiasedLocking::biased_lock_entry_count_addr())++;
        }
        success = true;
      }
      // class的prototype_header不是偏向模式
      else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
        // 尝试撤销偏向
                ...
      }
      // epoch过期,重新偏向
      else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
        // try rebias
                ...
        success = true;    
      }
      else {
        // try to bias towards thread in case object is anonymously biased
        // 尝试偏向该线程,只有匿名偏向能成功
        // 构建了匿名偏向的mark word
        markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |(uintptr_t)markOopDesc::age_mask_in_place |epoch_mask_in_place));
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
        // 用「或」操作设置thread ID
        markOop new_header = (markOop) ((uintptr_t) header | thread_ident);、
        // 只有匿名偏向才能成功
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
          // cas修改成功    
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
        }
        else {
          // 失败说明存在竞争,进入monitorenter
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
    }
    // 步骤4
    // traditional lightweight locking
    // false走轻量级锁逻辑
    if (!success) {
      // 构造一个无锁状态的Displaced Mark Word,并将lock record指向它
      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) {
        // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
        // Is it simple recursive case?
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          // 如果是锁重入,则直接将Displaced Mark Word设置为null
          // 轻量级锁重入是使用lock record的数量来计入的
          entry->lock()->set_displaced_header(NULL);
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    // 没拿到lock record,重新执行
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

偏向锁流程:

步骤 1 、从当前线程的栈中找到一个空闲的 Lock Record ,并指向当前锁对象。

步骤 2 、获取对象的markOop数据mark,即对象头的Mark Word;

步骤 3 、判断锁对象的 mark word 是否是偏向模式,即第3位是否为101。若不是,进入步骤4。若是,计算
anticipated_bias_locking_value ,判断偏向状态:

步骤 3.1 、
anticipated_bias_locking_value 若为0,代表 偏向的线程是当前线程 且 mark word 的epoch等于class的epoch,这种情况下直接执行同步代码块,什么都不用做。

步骤 3.2 、判断class的 prototype_header 是否为非偏向模式。若为非偏向模式,CAS尝试将对象恢复为无锁状态。无论cas是否成功都会进入轻量级锁逻辑。

步骤 3.3 、如果epoch偏向 时间戳已过期 ,则需要重偏向。利用CAS指令将锁对象的 mark word 替换为一个偏向当前线程且epoch为类的epoch的新的 mark word 。

步骤 3.4 、CAS将偏向线程改为当前线程,如果当前是 匿名偏向 (即对象头中的bit field存储的Thread ID为空)且 无并发冲突 ,则能 修改成功 获取偏向锁,否则进入 锁升级 的逻辑。

步骤 4 、走到一步会进行轻量级锁逻辑。构造一个无锁状态的 mark word ,然后存储到 Lock Record 。设置为无锁状态的原因是:轻量级锁解锁时是将对象头的 mark word cas替换为 Lock Record 中的 Displaced Mark Word ,所以设置为无锁状态。如果是锁重入,则将 Lock Record 的 Displaced Mark Word 设置为null,放到栈帧中,起到计数作用。

以上是偏向锁加锁的大致流程,如果当前锁 已偏向其他线程 || epoch值过期 || class偏向模式关闭 || 获取偏向锁的过程中存在并发冲突 ,都会进入到
InterpreterRuntime::monitorenter 方法, 在该方法中会进行偏向锁撤销和升级。流程如下图所示:

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 、到这说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁;

结合上文偏向锁的流程,可以整理得到如下的流程图:

2.4.2 轻量级锁的释放

轻量级锁释放的入口在bytecodeInterpreter.cpp#1923。

轻量级锁释放时需要将 Displaced Mark Word 替换回对象头的 mark word 中。如果CAS失败或者是重量级锁则进入到
InterpreterRuntime::monitorexit 方法中。 monitorexit 直接调用 slow_exit 方法释放 Lock Record 。直接看 slow_exit :

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(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 (elem == NULL || h_obj()->is_unlocked()) {
    THROW(vmSymbols::java_lang_IllegalMonitorStateException());
  }
  // 直接调用slow_exit
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  // Free entry. This must be done here, since a pending exception might be installed on
  // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
  elem->set_obj(NULL);
IRT_END

void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
  fast_exit (object, lock, THREAD) ;
}

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  ...
  // displaced header就是对象mark word的拷贝
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {
     // 什么也不做
     // Recursive stack-lock. 递归堆栈锁
     // Diagnostics -- Could be: stack-locked, inflating, inflated. 
     ...
     return ;
  }
  mark = object->mark() ;
  // 此处为轻量级锁的释放过程,使用CAS方式解锁。
  // 如果对象被当前线程堆栈锁定,尝试将displaced header和锁对象中的MarkWord替换回来。
  // If the object is stack-locked by the current thread, try to
  // swing the displaced header from the box back to the mark.
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  //走到这里说明已经是重量级锁或者解锁时发生了竞争,膨胀后再调用monitor的exit方法释放
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

最后执行的是如果是fast_exit方法。如果是轻量级锁,尝试cas替换 mark word 。若解锁时有竞争,会调用 inflate 方法进行重量级锁膨胀,升级到到重量级锁后再执行 exit 方法。

2.5 重量级锁

2.5.1 重量级锁的进入

重量级锁通过对象内部的监视器(monitor)实现,其依赖于底层操作系统的 Mutex Lock 实现,需要额外的用户态到内核态切换的开销。由上文分析, slow_enter 获取轻量级锁未成功时,会在 inflate 中完成锁膨胀:

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  ...
  for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;  
      // mark是以下状态中的一种:
      // *  Inflated(重量级锁状态)     - 直接返回
      // *  Stack-locked(轻量级锁状态) - 膨胀
      // *  INFLATING(膨胀中)    - 忙等待直到膨胀完成
      // *  Neutral(无锁状态)      - 膨胀
      // *  BIASED(偏向锁)       - 非法状态,在这里不会出现

      // CASE: inflated
      if (mark->has_monitor()) {
          // 已经是重量级锁状态了,直接返回
          ObjectMonitor * inf = mark->monitor() ;
          ...
          return inf ;
      }
      // CASE: inflation in progress
      if (mark == markOopDesc::INFLATING()) {
         // 正在膨胀中,说明另一个线程正在进行锁膨胀,continue重试
         TEVENT (Inflate: spin while INFLATING) ;
         // 在该方法中会进行spin/yield/park等操作完成自旋动作 
         ReadStableMark(object) ;
         continue ;
      }
      // 当前是轻量级锁,后面分析
      // CASE: stack-locked
          if (mark->has_locker()) {
        ...
      }
      // 无锁状态
      // CASE: neutral
      // 分配以及初始化ObjectMonitor对象
      ObjectMonitor * m = omAlloc (Self) ;
      // prepare m for installation - set monitor to initial state
      m->Recycle();
      m->set_header(mark);
      // owner为NULL
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
        // 用CAS替换对象头的mark word为重量级锁状态
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          // 不成功说明有另外一个线程在执行inflate,释放monitor对象
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
          // interference - the markword changed - just retry.
          // The state-transitions are one-way, so there's no chance of
          // live-lock -- "Inflated" is an absorbing state.
      }

      ...
      return m ;
}

inflate 其中是一个for循环,主要是为了处理多线程同时调用inflate的情况。然后会根据锁对象的状态进行不同的处理:

  1. 已经是重量级状态,说明膨胀已经完成,返回并继续执行ObjectMonitor::enter方法。
  2. 如果是轻量级锁则需要进行膨胀操作。
  3. 如果是膨胀中状态,则进行忙等待。
  4. 如果是无锁状态则需要进行膨胀操作。

轻量级锁膨胀流程如下:

if (mark->has_locker()) {
  // 步骤1
  // 当前轻量级锁状态,先分配一个ObjectMonitor对象,并初始化值
  ObjectMonitor * m = omAlloc (Self) ;          
  m->Recycle();
  m->_Responsible  = NULL ;
  m->OwnerIsThread = 0 ;
  m->_recursions   = 0 ;
  m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
  // 步骤2
  // 将锁对象的mark word设置为INFLATING (0)状态 
  markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
  if (cmp != mark) {
    omRelease (Self, m, true) ;
    continue ;       // Interference -- just retry
  }
  // 步骤3
  // 栈中的displaced mark word
  markOop dmw = mark->displaced_mark_helper() ;
  assert (dmw->is_neutral(), "invariant") ;
  // 设置monitor的字段
  m->set_header(dmw) ;
  // owner为Lock Record
  m->set_owner(mark->locker());
  m->set_object(object);
  ...
  // 步骤4
  // 将锁对象头设置为重量级锁状态
  object->release_set_mark(markOopDesc::encode(m));
  ...
  return m ;
}

步骤 1 、调用 omAlloc 获取一个可用的 ObjectMonitor 对象。在 omAlloc 方法中会先从 线程私有 的 monitor 集合 omFreeList 中分配对象,如果 omFreeList 中已经没有 monitor 对象,则从 JVM全局 的 gFreeList 中分配一批 monitor 到 omFreeList 中;

步骤 2 、通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,标识当前锁正在膨胀中。如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成。

步骤 3 、如果CAS成功,设置monitor的各个字段:设置 monitor 的header字段为 displaced mark word ,owner字段为 Lock Record ,obj字段为锁对象等;

步骤 4 、设置锁对象头的 mark word 为重量级锁状态,指向第一步分配的 monitor 对象;

2.5.2 monitor竞争

当锁膨胀 inflate 执行完并返回对应的 ObjectMonitor 时,并不表示该线程竞争到了锁,真正的锁竞争发生在 ObjectMonitor::enter 方法中。

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  // 步骤1
  // owner为null,如果能CAS设置成功,则当前线程直接获得锁
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     ...
     return ;
  }
  // 如果是重入的情况
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
  // 步骤2
  // 如果当前线程是之前持有轻量级锁的线程
  // 上节轻量级锁膨胀将owner指向之前Lock Record的指针
  // 这里利用owner判断是否第一次进入。
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    // 重入计数重置为1
    _recursions = 1 ;
    // 设置owner字段为当前线程
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
  ...
  // 步骤3
  // 在调用系统的同步操作之前,先尝试自旋获得锁
  if (Knob_SpinEarly && TrySpin (Self) > 0) {    
     ...
     //自旋的过程中获得了锁,则直接返回
     Self->_Stalled = 0 ;
     return ;
  }
  ...
  { 
    ...
    // 步骤4
    for (;;) {
      jt->set_suspend_equivalent();
      // 在该方法中调用系统同步操作
      EnterI (THREAD) ;
      ...
    }
    Self->set_current_pending_monitor(NULL); 
  }
  ...
}

步骤 1 、当前是无锁、锁重入,简单操作后返回。

步骤 2 、当前线程是之前持有轻量级锁的线程,则为首次进入,设置recursions为1,owner为当前线程,该线程成功获得锁并返回。

步骤 3 、先 自旋尝试 获得锁,尽可能减少同步操作带来的开销。

步骤 4 、调用EnterI方法。

这里注意,轻量级锁膨胀成功时,会把owner字段设置为 Lock Record 的指针,并在竞争时判断。这么做的原因是,假设当前线程A持有锁对象的锁,线程B进入同步代码块,并把锁对象升级为重量级锁。但此时,线程A可能还在执行,并无法感知其持有锁对象的变化。因此,需要线程B在执行 ObjectMonitor::enter 时,将自己放入到阻塞等列等待。并需要线程A第二次进入、或者退出的时候对monitor进行一些操作,以此保证代码块的同步。

这里有个 自旋 操作,直接看 TrySpin 对应的方法:

// TrySpin对应的方法
int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
    // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.
    int ctr = Knob_FixedSpin ;  // 固定自旋次数
    if (ctr != 0) {
        while (--ctr >= 0) {
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }
    // 上一次自旋次数
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {  // 尝试获取锁
        // Increase _SpinDuration ...
        // Note that we don't clamp SpinDuration precisely at SpinLimit.
        // Raising _SpurDuration to the poverty line is key.
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      ...
      ...

从方法名和注释可以看出,这就是自适应自旋, 和网上说的轻量级锁cas失败会自旋的说法并不一致 。实际上,无论是轻量级锁cas自旋还是重量级锁cas自旋,都是在用户态尽可能减少同步操作带来的开销,并没有太多本质上的区别。到此为止,我们可以再结合上述的内容,整理出如下的状态转换图:

2.5.3 monitor等待

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 为双向链表。

当一个线程尝试获得重量级锁且没有竞争到时,该线程会被封装成一个 ObjectWaiter 对象插入到cxq的队列的队首,然后调用 park 函数挂起当前线程,进入BLOCKED状态。当线程释放锁时,会根据唤醒策略,从cxq或EntryList中挑选一个线程 unpark 唤醒。如果线程获得锁后调用 Object#wait 方法,则会将线程加入到WaitSet中,进入WAITING或TIMED_WAITING状态。当被 Object#notify 唤醒后,会将线程从WaitSet移动到cxq或EntryList中去,进入BLOCKED状态。需要注意的是,当调用一个锁对象的 wait 或 notify 方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

2.5.4 monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放。在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于 ObjectMonitor::exit 方法中。

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
   // 如果_owner不是当前线程
   if (THREAD != _owner) {
     // 轻量级锁膨胀上来,还没调用过enter方法,_owner还指向之前轻量级锁Lock Record的指针。
     if (THREAD->is_lock_owned((address) _owner)) {
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       // 异常情况:当前不是持有锁的线程
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
   // 重入计数器还不为0,则计数器-1后返回
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }
   ...
   //这块开始是唤醒操作
   for (;;) {
     ...
     ...
     ObjectWaiter * w = NULL ;
     // 根据QMode的不同会有不同的唤醒策略,默认为0
     int QMode = Knob_QMode ;
     if (QMode == 2 && _cxq != NULL) {
          ...
          ...

步骤 1 、处理owner不是当前线程的状况。这里特指之前持有轻量级锁的线程,由于没有调用过enter,owner指向仍为Lock Record的指针,以及其他异常情况。

步骤 2 、重入计数器还不为0,则计数器-1后返回。

步骤 3 、唤醒操作。根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。

根据QMode的不同(默认为0),有不同的处理方式:

QMode = 0:暂时什么都不做;

QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;

QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;

QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;

只 在QMode=2的时候会提前返回,等于0、3、4的时继续执行:

  1. 如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
  2. 如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回;
  3. 被唤醒的线程,继续竞争monitor。

2.6 本章小节

本章介绍了Synchronized的底层实现和锁升级过程。对于锁升级,再看看本文整理的图,一图胜千言:

这里有几个点可以注意一下:

  1. HotSpot中,只用到了 模板解释器 ,并没有用到字节码解释器, monitorenter 的实际入口位于templateTable_x86_64.cpp#3667。本文的分析是基于字节码解释器的,因此部分结论不能作为实际执行情况。本章的内容只能作为Synchronized锁升级原理、各类锁的适用场景的一种 窥探 。
  2. 再次强调,无锁状态只能升级为轻量级锁, 匿名偏向状态 才能进入到偏向锁。
  3. 偏向锁 并不都有利, 其适用于 单个线程重入 的场景,原因为:偏向锁的撤销需要进入 safepoint ,开销较大。需要进入 safepoint 是由于,偏向锁的撤销需要对锁对象的 lock record 进行操作,而 lock record 要到每个线程的栈帧中遍历寻找。在非safepoint,栈帧是动态的,会引入更多的问题。目前看来,偏向锁存在的价值是为历史遗留的Collection类如Vector和HashTable等做优化,迟早药丸。Java 15中默认不开启。
  4. 执行Object类的 hashcode 方法,偏向锁撤销并且锁会膨胀为轻量级锁或者重量锁。执行Object类的 wait/notify/notifyall 方法,偏向锁撤销并膨胀成重量级锁。
  5. 轻量级锁适用于 两个线程的交替执行 场景:线程A进入轻量级锁,退出同步代码块并释放锁,会将锁对象恢复为无锁状态;线程B再进入锁,发现为无锁状态,会cas尝试获取该锁对象的轻量级锁。如果有竞争,则直接膨胀为重量级锁,没有自旋操作,详情看10。
  6. 唤醒策略依赖于 QMode 。重量级锁获取失败后,线程会加入cxq队列。当线程释放锁时,会从cxq或EntryList中挑选一个线程唤醒。线程获得锁后调用 Object#wait 方法,则会将线程加入到WaitSet中。当被 Object#notify 唤醒后,会将线程从WaitSet移动到cxq或EntryList中去。
  7. 重量级锁,会将线程放进等待队列,等待操作系统调度。而偏向锁和轻量级锁,未交由操作系统调度,依然处于用户态,只是采用CAS无锁竞争的方式获取锁。CAS通过Unsafe类中compareAndSwap方法,jni调用C++方法,通过汇编指令锁住cpu中的北桥信号。
  8. 许多文章声称一个对象关联到一个monitor,这个说法不够准确。如果对象已经是重量级锁了,对象头的确指向了一个 monitor 。但对于正在膨胀的锁,会先从 线程私有 的 monitor 集合 omFreeList 中分配对象。如果 omFreeList 中已经没有 monitor 对象,再从 JVM全局 的 gFreeList 中分配一批 monitor 到 omFreeList 中。
  9. 在编译期间还有 锁消除 和 锁粗化 这两步锁优化操作,本章没做介绍。
  10. 字节码实现中没有体现轻量级锁自旋逻辑。这可能是模板解释器中的实现,或者是jvm在不同平台、不同jvm版本的不同实现。但本文分析的字节码链路中没有发现该逻辑,倒是发现了 重量级锁会自适应自旋竞争锁 。因此个人对轻量级锁自适应自旋的说法存疑,至少hotspot jdk8u字节码实现中没有这个逻辑。但两者都是在用户态进行自适应自旋,以尽可能减少同步操作带来的开销,没有太多本质上的区别,并不需要特别关心。

三、线程的实现与状态转换

3.1 线程的实现

(1)内核线程实现

内核线程(Kernel-Level Thread,KLT):由 内核 来完成线程切换,内核通过 调度器 对线程进行调度,并负责将线程的任务 映射 到各个处理器上。程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口—— 轻量级进程 (Light Weight Process,LWP),也就是通常意义上的线程。

优点:每个LWP都是独立的调度单元。一个LWP被阻塞,不影响其他LWP。

缺点:基于KLT,耗资源。线程的创建、析构、同步都需要进行系统调用,频繁的用户态、内核态切换。

(2) 用户线程实现(User Thread,UT)

广义:非内核线程,都可认为是用户线程。(包括LWT,虽然LWT的大多操作都要映射到KLT)

狭义:完全建立在 用户空间 的线程库上,系统内核不能感知线程存在的实现。UT也只感知到掌管这些UT的进程P。

优点:用户线程的创建、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。

缺点:线程的创建、销毁、切换和调度都是用户必须考虑到问题。“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难。

(3) 混合实现 混合模式下, 即存在用户线程,也存在轻量级进程 。用户线程的创建、切换、析构等操作依然廉价,可以支持大规模的用户线程并发,且可以使用内核线程提供的线程调度功能及处理器映射。

线程的实现依赖操作系统支持的线程模型。在主流的操作系统上,hotspot、classic、art等虚拟机默认是 1:1的线程模型。在Solaris平台上,hotspot支持1:1、N:M两种线程模型。

3.2 线程的转换

首先明确一点,当我们讨论一个线程的状态,指的是Thread 类中threadStatus的值。

private volatile int threadStatus = 0;

该值映射后对应的枚举为:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

也就是说,线程的具体状态,看threadStatus就行了。

NEW

先要创建Thread 类的对象,才能谈其状态。

Thread t = new Thread();

这个时候,线程t就处于新建状态。但他还不是“线程”。

RUNNABLE

然后调用start()方法。

t.start();

调用start()后,会执行一个native方法创建内核线程,以linux为例:

private native void start0();

// 最后走到这
hotspot/src/os/linux/vm/os_linux.cpp
pthread_create(...);

这时候才有一个真正的线程创建出来,并即刻开始运行。这个内核线程与线程t进行1:1的映射。这时候t具备运行能力,进入RUNNABLE状态。RUNNABLE可以细分为READY和RUNNING,两者的区别只是是否等待到了资源并开始运行。处于RUNNABLE且未运行的线程,会进入一个就绪队列中,等待操作系统的调度。处于就绪 队列的线程都在等待资源,这个资源可以是cpu的时间片、也可以是系统的IO。JVM并不关系READY和RUNNING这两种状态,毕竟上述的枚举类都不对RUNNABLE进行细分。

TERMINATED

当一个线程执行完毕(或者调用已经不建议的 stop 方法),线程的状态就变为 TERMINATED。进入TERMINATED后,线程的状态不可逆,无法再复活。

关于BLOCKED、WAITING、TIMED_WAITING

BLOCKED、WAITING、TIMED_WAITING都是带有同步语义的状态,我们先看一下 wait 和 notify 方法的底层实现。wait方法的底层实现:

void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
    ...
    ...
  //获得Object的monitor对象
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  //调用monitor的wait方法
  monitor->wait(millis, true, THREAD);
    ...
}

  inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
    ...
  if (_WaitSet == NULL) {
    //_WaitSet为null,就初始化_waitSet
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    //否则就尾插
    ObjectWaiter* head = _WaitSet ;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}

主要流程:通过object获得objectMonitor,将Thread封装成OjectWaiter对象,然后 addWaiter 将它插入 waitSet 中,进入waiting或timed_waiting状态。最后释放锁,并通过底层的 park 方法挂起线程;

notify方法的底层实现:

void ObjectSynchronizer::notify(Handle obj, TRAPS) {
    ...
    ...
    ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}
    //通过inflate方法得到ObjectMonitor对象
    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
    ...
     if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is inva;lid");
          return inf 
      }
    ...
      }
    //调用ObjectMonitor的notify方法
    void ObjectMonitor::notify(TRAPS) {
    ...
    //调用DequeueWaiter方法移出_waiterSet第一个结点
    ObjectWaiter * iterator = DequeueWaiter() ;
    //将上面DequeueWaiter尾插入_EntrySet或cxq等操作
    ...
    ...
  }

通过object获得objectMonitor,调用objectMonitor的 notify 方法。这个notify最后会走到
ObjectMonitor::DequeueWaiter 方法,获取waitSet列表中的第一个ObjectWaiter节点。并根据不同的策略,将取出来的ObjectWaiter节点,加入到 EntryList 或 cxq 中。 notifyAll 的实现类似于 notify ,主要差别在多了个for循环。

由这里以及上一章2.5.4 monitor释放小节中可以了解到, notify 和 notifyAll 并不会立即释放所占有的ObjectMonitor对象,其真正释放ObjectMonitor的时间点是在执行 monitorexit 指令。

一旦释放 ObjectMonitor 对象了, entryList 和 cxq 中的ObjectWaiter节点会依据 QMode 所配置的策略,通过ExitEpilog方法唤醒取出来的ObjectWaiter节点。被唤醒的线程,继续参与monitor的竞争。若竞争失败,重新进入BLOCKED状态,再回顾一下monitor的核心结构。

既然聊到了 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资源是等价的。

这里有几个点可以注意一下:

  1. JVM线程状态不代表内核线程状态;
  2. BLOCKED的线程一定处于entryList或cxq中,而处于WAITING和TIMED WAITING的线程,可能是由于执行了sleep或park进入该状态,不一定在waitSet中。也就是说,处于BLOCKED状态的线程一定是与同步相关。由这可延伸出,调用 jdk 的 lock并获取不到锁的线程,进入的是 WAITING 或 TIMED_WAITING 状态,而不是BLOCKED状态。
汇川伺服Modbus-RTU通讯实战:从帧结构解析到西门子PLC配置 本文详细解析了汇川伺服与西门子PLC通过Modbus-RTU协议进行通讯的实战步骤,包括帧结构解析、硬件连接、参数设置及PLC程序配置。通过具体案例和常见问题排查指南,帮助工程师快速掌握工业自动化中的通讯技术,提升设备调试效率。 阅读详情

相关推荐

Latex | IEEE双栏排版中长公式的排版

IEEE双栏排版中,有时候公式过于长了,过宽,导致单栏排版不下。

qq_43466146的博客 1万+

Linux原子操作 atomic_cmpxchg()/Atomic_read()/Atomic_set()/Atomic_add()/Atomic_sub()/atomi

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位。因此这里的原子实际是使用了物理学里的物质微粒的概念。 原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。 原子操作主要用于实现资源计

strugglelg的专栏 9686

EPSON RX8010SJ RTC实战:I2C寄存器配置避坑指南(附完整初始化代码)

本文详细解析了EPSON RX8010SJ RTC芯片在I2C寄存器配置中的常见问题与解决方案,包括TEST位、STOP位和中断控制等关键配置要点。通过实战经验分享和完整的初始化代码模板,帮助开发者避免常见陷阱,确保RTC模块的稳定运行。

weixin_28678831的博客 407

Linux并发与同步--基础知识

Linux并发与同步–基础知识

yaoming168的专栏 1595

FPGA远程升级避坑指南:用AXI Quad SPI IP读写N25Q128 Flash的完整流程

本文详细介绍了FPGA远程升级过程中使用AXI Quad SPI IP读写N25Q128 Flash的完整流程,包括IP核配置、Flash操作指令集、双镜像备份设计及生产级远程升级协议。通过最佳实践和避坑策略,帮助开发者实现可靠的镜像更新方案,提升系统可靠性和可维护性。

weixin_30542079的博客 676

Vue2 - 实现H5人脸识别认证与活体检测功能(纯前端免费方案+微信人脸识别接口)完整源码,vue微信公众号网页h5端人脸识别功能人脸核身(微信方案提供后端spring boot+thinkphp)

vue人脸识别插件,vue, h5人脸识别, 微信公众号, 活体检测, 人脸核身,人脸登录、人脸识别,VUE项目-H5端人脸识别功能实现,免费h5人脸识别插件方案,微信公众号H5网页实现人脸识别,vue人脸核身,Vue H5活体检测认证,人脸认证张嘴摇摇头指引动作,人脸核身 微信原生 H5(微信公众号)vue2调用手机摄像头并实现人脸识别的实现,微信h5人脸识别vue项目,PHP人脸识别,java人脸识别,h5页面,有免费的人脸识别接口吗,用于微信公众号及嵌套在微信浏览器,vue3如何实现人脸识别,微信人脸

🏆 前端领域优质创作者 3763

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程

前言 上篇文章已经分析了Java对象头构成、源码及其对象头的调试,本篇将分析偏向锁、轻量级锁、重量级锁的实现及其演变过程。由于涉及到c++源码,估计不少同学没兴趣看,因此重点多以图+源码辅助分析。 通过本篇文章,你将了解到: 1、什么是重量级锁 2、轻量级锁/偏向锁的由来 3、偏向锁的加锁、撤销锁、释放锁 4、轻量级锁的加锁、释放锁 5、偏向锁、轻量级锁、重量级锁的异同点 1、什么是重量级锁 简单例子 private synchronized void testLock() {

7922

【Docker安装】Ubuntu 24.04.2 LTS系统下安装Docker环境——指定APT源安装方式

【Docker安装】Ubuntu 24.04.2 LTS系统下安装Docker环境——指定APT源安装方式

jks212454的博客 626

线程们,请排队!Java 锁的那些事儿

偏向锁:单线程访问时零开销轻量级锁:短时间竞争自旋,减少挂起重量级锁:长时间竞争,保证安全自适应自旋:智能选择自旋或阻塞,平衡性能和安全偏向锁(Biased Locking)↓ 竞争出现轻量级锁(Lightweight Lock / 自旋)↓ 自旋失败重量级锁(Heavyweight Lock / 阻塞)JVM 的锁升级机制是一套“智慧守门员策略”,自动在低竞争、高性能 vs 高竞争、安全性之间切换,让我们用最少的成本保证线程安全。锁类型独占/共享可重入公平性可中断优点缺点典型场景。

weixin_44876263的博客 1209

ABAP CDS 和 HANA SQL 的关系

摘要: ABAP CDS与HANA SQL既有紧密联系又有本质区别。ABAP CDS作为语义数据建模框架,在SQL基础上扩展了业务语义层,通过注解(annotations)、关联关系(associations)等机制为数据附加业务含义,并支持权限控制、客户端处理等ABAP平台特性。虽然CDS实体最终会转换为HANA数据库对象执行,但它并非简单的SQL封装,而是构建了独立于数据库的语义模型。二者在数据库执行层深度融合,ABAP CDS通过代码下推技术将计算任务优化到HANA层完成。实际开发中应优先使用ABAP

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 863

【Claude】Not logged in 错误:未登录状态的判断与 /login 认证路径 bug报错已解决

login 是首选修复:大多数 "Not logged in" 只需重新执行 /login检查系统时间:时间偏差是 Token 验证失败的常见原因避免密钥库被清理:将 Claude Code 添加到系统清理工具的白名单企业环境用 API Key:如果组织禁用 OAuth,使用 API Key 认证区分凭证类型:理解 Not logged in(无凭证)vs Invalid key(有但无效)

博客 7317

可重入锁和不可重入锁的区别

不可重入锁示例(同一个线程不可以重入上锁后的代码段) 如下是一个不可重入锁的逻辑过程,会发现执行main方法控制台会打印执行doJob方法前,然后就会一直线程阻塞,不会打印执行doJob方法过程中,原因在于第一次上锁后,由于没有释放锁,因此执行第一次lock后isLocked = true,这个时候调用doJob()内部又一次调用了lock()由于上个线程将isLocked = true,导致再次进入的时候就进入死循环。导致线程无法执行System.out.println("执行doJob方法过程中");这

诗水人间 8万+

5.3-集群电源模式解析

DSU-120 DynamIQ™集群支持9种电源模式,包括正常运行(ON)、完全断电(OFF)、三种数据保持模式(FUNC_RET、FULL_RET、MEM_RET)、两种调试模拟模式(OFF_EMU、MEM_RET_EMU),以及系统恢复模式(WARM_RST、DBG_RECOV)。这些模式通过智能管理电源状态,在保持数据完整性的同时实现功耗优化,并为系统调试和故障恢复提供支持。不同模式在RAM保持、逻辑断电和调试能力方面各有侧重,满足从正常运行到深度调试等不同场景需求。

baron-周贺贺-代码改变世界ctw 117

Java多线程 各种锁(一篇全搞懂)

Java多线程 锁 文章目录Java多线程 锁1、乐观锁与悲观锁2、公平锁与非公平锁3、可重入锁与不可重入锁4、独享锁与共享锁5、自旋锁 VS 适应性自旋锁6、无锁 、 偏向锁、量级锁 和 重量级锁(难点) 1、乐观锁与悲观锁 (1)悲观锁 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,syn...

烟雨gps 5953

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

多线程-可重入锁与不可重入锁

什么是可重入锁与不可重入锁? “可重入锁”这四个字分开来解释: 可:可以。 重:再次。 入:进入。 锁:同步锁。 综上所述,“可重入锁”就是这把同步锁可以再次进入。 进入什么? 进入同步域(即同步代码块/方法或显式锁锁定的代码)。 通俗来讲,可重入锁就是一证通。 只需一个证就可以通过所有相同关卡: 不可重入锁就是:即使每个关卡相同,你也得再拿一个一摸一样的证...

天道酬勤 6312

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

不可不说的 Java “锁”事

文章目录前言1. 乐观锁 VS 悲观锁2. 自旋锁 VS 适应性自旋锁3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁无锁偏向锁轻量级锁重量级锁4. 公平锁 VS 非公平锁5. 可重入锁 VS 非可重入锁6. 独享锁 VS 共享锁结语 前言 Java 提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自 JDK 8 和 Ne...

安正勋的博客 1115

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Java并发线程安全与锁优化(不可变 绝对线程安全 相对线程安全 线程兼容 线程对立 互斥同步 非阻塞同步 自旋锁 自适应锁 锁消除 锁粗化 轻量级锁 偏向锁 CAS synchronized)

线程安全与锁优化 文章目录线程安全与锁优化一、线程安全概念不可变绝对线程安全相对线程安全线程兼容线程对立二、线程安全的实现方法互斥同步最基本互斥手段就是synchronized另一种手段JUC下的ReentrantLock阻塞同步的问题:非阻塞同步实现方式CAS操作:无同步方案三、锁优化自旋锁与自适应锁锁消除锁粗化轻量级锁偏向锁 一、线程安全概念 为了更好的理解线程安全,我们不把线程安全看做是一个二元对立的选项来看,而是按照线程安全的”安全程度“由强至弱来排序。 Java线程各种操作共享的数据分为五类:

千里之行,始于足下 1407

java可重入锁与不可重入锁

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。 synchronized 和   ReentrantLock 都是可重入锁。 可重入锁的意义在于防止死锁。 实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1...

慎独 2万+

Java不可重入锁和可重入锁理解

最近正在阅读Java ReentrantLock源码,始终对可重入和不可重入概念理解不透彻,进行学习后记录在这里。 基础知识 Java多线程的wait()方法和notify()方法 这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出IllegalMonitorStateException异常,所以这两个方法必须在同步...

u012545728的博客 6万+

由JVM深入了解Java的线程安全与锁优化

目录 •写在前面 •线程安全 Java语言中的线程安全 线程安全的实现方法 •锁优化 自旋锁与自适应自旋 锁消除 锁粗化 轻量级锁 偏向锁 •写在前面 讲道理,在谈及线程安全以及锁优化之前,需要先搞清楚啥是线程,像那些硬概念这里我就不多说,什么线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度(...

BoCong-Deng的博客 1万+

【躲不过的Java “锁事”】一文扫除对Java各种锁的困扰!

- 从各种锁的设计,抽象出的概览思想可以分为 **悲观锁** 和 **乐观锁** - 根据线程获取锁的抢占机制,和锁的公平性又可以分为**公平锁** 和 **非公平锁**。 - 从根据锁是否重复获取可以分为 **可重入锁** 和 **不可重入锁** - 根据锁能否被多个线程持有,可以把锁分为**独占锁(排他锁)**和**共享锁 - 根据Synchronized锁升降级的状态可以分为 偏向锁 / 轻量级锁 / 重量级锁 - 从资源已被锁定,获取锁的阻塞装填可以分为 **自旋锁** 分段锁 AQS CAS

OxYGC 2153

Java 线程安全与锁优化

什么是线程安全? 线程安全经常会被各种行业大佬或者面试官大佬挂在嘴边,如何找到一个通俗易懂一点的方式解释线程安全呢,伟大的砖家给出了答案:如果一个对象可以安全的被多个对象使用,那它就是线程安全的。是的,这种回答非常的巧妙精彩…巧妙到你无法怼它是错误的…但是也无法从中找到任何有用信息… 众多定义中,《Java Concurrency In Practice》的作者Brian Goetz对线程安全...

一只猿的自我修养 3530

【Java高效并发系列】- 线程安全与锁优化

本章关注虚拟机如何实现“高效”,以及虚拟机对并发代码提供了什么样的优化手段高效并发,首先需要保证并发的正确性,然后在此基础上实现高效。

m0_68383079的博客 883

多线程synchronized——线程“八锁”

所谓的线程八锁:其实就是从八段代码例子中,理解synchronized的用法,不要太纠结这个名称,不需要纠结八种情况里面好像有些情况是相似的,只需要理解这八个代码例子里面的synchronized 的用法,其他都不用管。(其实就是考察 synchronized 锁住的是哪个对象)

学习笔记 772
上一篇: Spring源码编译一次性通过&遇到的坑解决方法
下一篇: 一杯茶的功夫就把日志搜索引擎性能调优了
JavaShark
博客等级 码龄4年 184粉丝 438原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值