Java-并发-锁-synchronized

19 篇文章 0 订阅
13 篇文章 0 订阅

Java-并发-锁-synchronized

摘要

本文会详细说下synchronized的底层实现原理。

0x01 基本概念

  • 每次只能有一个线程进入临界区
  • 保证临界区内共享变量的可见性和有序性
  • 成功进入synchronized区域的线程可以拿到对象的Object-Monitor。具体有3种用法,作用域不同,在后面例子中介绍。
  • 对于拿到锁的线程来说,同一个对象的synchronized具有可重入性
  • 不要将可变对象作为synchronized
  • 如果相互等待对方的synchronized 对象,可能出现死锁
  • synchronized锁是非公平的

注意:最近发现本文所讲偏向锁和轻量级锁的代码分析章节有误,请大家移驾参阅死磕Synchronized底层实现–概论系列文章,查看源码分析。待后续有时间我会改正本文内容。

也可以看深入理解Java并发之synchronized实现原理

前置知识,是锁优化升级:

  • Java中4种synchronized锁状态,随着锁竞争开始,这几种锁之间有锁升级的关系:
    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
  • 锁只能升级不能降级。这么做的原因是缩短锁的获取释放周期,提升效率。

关于锁优化和锁升级知识的更多详细内容请点击此文:Java-并发-关于锁的一切

0x02 实现原理

下图中:

  • 等待队列表示使用synchronized获取对象锁后,又调用wait方法的对象进入的队列;

  • 同步队列表示正在使用synchronized竞争、等待对象锁的队列

  • 每次只能有一个对象抢占对象锁成功,其他竞争者继续在同步队列中等待
    在这里插入图片描述

  • 步骤6如果是调用的notify方法,任意唤醒一个在对象锁的等待集的线程(其实看了源码会发现不是任意的,而是一个WaitQueue,FIFO),进入同步队列。

    但要注意,此时被唤醒的线程不会马上开始运行,因为对象锁还被调用notify的线程拥有,直到退出synchronized块。

    唤醒后的线程跟其他线程一起竞争该同步对象锁。

2.1 引子-synchronized用于对象同步块

可以用一个简单代码试试:

public class SimpleTest2
{

    public static void main(String[] args)
    {
        Object chengc = new Object();
        synchronized (chengc){
            int i = 1;
            System.out.println("");
        }
    }
}

然后就是用命令编译:

$ javac SimpleTest2.java

然后进行反汇编:

$ javap -c SimpleTest2.class

结果如下:

11: monitorenter
12: iconst_1
13: istore_3
14: aload_2
15: monitorexit

synchronized代码块通过javap生成的字节码中包含 monitorentermonitorexit指令。也就是说,synchronized指令可以尝试获取对象锁(object-monitor):

  • monitor
    每个对象都关联着一个monitor,只能被唯一获取monitor权限的线程锁定。锁定后,其他线程请求会失败,进入等待集合,线程随之被阻塞。
  • monitorenter
    这个命令就是用来获取监视器的权限,每进入1次就记录次数加1,也就是同一线程说可重入。而其他未获取到锁的只能等待。
  • monitorexit
    拥有该监视器的线程才能使用该指令,且每次执行都会将累计的计数减1,直到变为0就释放了所有权。在此之后其他等待的线程就开始竞争该监视器的权限

jdk8/hotspot/src/share/vm/interpreter/interpreterRuntime.hpp中可以找到如下方法:

static void    monitorenter(JavaThread* thread, BasicObjectLock* elem);
static void    monitorexit (JavaThread* thread, BasicObjectLock* elem);

因为前置知识还不够,我们代码分析先说到这里。请看完第四章后,在这里继续看monitorenter底层实现。

2.2 synchronized用于方法

先说结论,将关键字修饰方法,那就会使得该方法的flags中多出一个ACC_SYNCHRONIZED。当线程执行时就需要去获取对象监视器,拿到的再开始执行方法。

实例如下:

public class SynchronizedMethodTest
{
    public synchronized void synMethod(){
        int j = 11;
    }

    public static synchronized void method() {
        System.out.println("Hello World!");
    }
}

编译后再使用javap -verbose SynchronizedMethodTest.class,部分结果如下:

public synchronized void synMethod();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=1, locals=2, args_size=1
       0: bipush        11
       2: istore_1
       3: return
    LineNumberTable:
      line 9: 0
      line 10: 3

public static synchronized void method();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=0, args_size=0
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World!
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
    LineNumberTable:
      line 13: 0
      line 14: 8

可以看到两个synchronized方法的flags区域都多了ACC_SYNCHRONIZED。有这种标识的,会被JVM要求执行方法之前先获取到锁,否则等待阻塞。

2.3 synchronized用于静态方法或类对象

这种情况的ObjectMonitor就是该Class对象。

2.4 ObjectMonitor

每个对象都有一个对象监视器。

Object-Monitor在HotSpot中实现主要在

  • /Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.hpp
  • /Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.cpp
    这里摘录一些重要的部分:
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

注意_WaitSett和_EntryList,他们被用来存储该Object的等待对象的集合,具体来说:

  • _count记录已获取锁的次数
  • _WaitSet存放处于wait状态的线程
  • _EntryList存放那些等待synchronized同步锁的线程
  • _owner是指向获得了该对象的Monitor权限的线程的指针。
  • _WaitSetLock用来对WaitSet进行同步保护

他们的关系示意图如下:
ObjectMonitor

  1. 执行synchronized,进入EntrySet,线程处于等待状态
  2. 每次都只能有一个线程获取到ObjectMonitor_owner指向该线程,处于Active状态。同时,将monitor计数器加1(退出一个同步块时,monitor计数器减1)
  3. 执行wait方法,会释放ObjectMonitor,进入WaitSet,线程处于等待状态。同时,_owner置为空,monitor计数器归0。
  4. 执行当拥有ObjectMonitor权限的线程释放后会,如果调用了notify或notifyAll方法,会唤醒WaitSet中的线程。被唤醒的线程重新竞争同步锁。
  5. 当退出synchronized块后,完全释放ObjectMonitor

2.5 ObjectWaiter

而EntryList和WaitSet中的等待对象ObjectWaiter实现如下:

class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next;
  ObjectWaiter * volatile _prev;
  Thread*       _thread;
  jlong         _notifier_tid;
  ParkEvent *   _event;
  volatile int  _notified ;
  volatile TStates TState ;
  Sorted        _Sorted ;           // List placement disposition
  bool          _active ;           // Contention monitoring is enabled
 public:
  ObjectWaiter(Thread* thread);

  void wait_reenter_begin(ObjectMonitor *mon);
  void wait_reenter_end(ObjectMonitor *mon);
};
  • TStates
    存放当前线程状态
  • _next和_prev
    可以看到该对象有两个分别指向前一个元素和后一个元素的指针,也就是说是一个双向链表。
  • _thread
    指向的线程

2.6 monitorenter源码分析

2.6.1 字节码解释器与模板解释器

HotSpot代码中有两个解释器来解析jvm指令。前者是用C++实现每条JVM指令,但执行较慢。代码在jdk8/hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp;后者(可参考这篇文章:JVM之模板解释器)对每个指令都写了一段汇编,启动时指令与汇编绑定,效率很高。代码在jdk8/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

但在HotSpot中只用了模板解释器,所以我们直接看templateTable_x86_64.cpp,我们要看的monitorentermonitorexit方法就在这里定义。

2.6.2 TemplateTable::monitorenter
2.6.1 InterpreterRuntime::monitorenter

对应到jdk8/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp继续看看monitorenter方法实现,这里摘录部分对我们分析有意义的代码如下:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
// 这个h_obj封装了该线程和拥有锁的对象
Handle h_obj(thread, elem->obj());
if (UseBiasedLocking) {
  // 当开启了偏向锁模式时,就优先使用快速进入模式避免不必要的锁膨胀
  ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
  ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
2.6.2 BasicObjectLock

上面的monitorenter方法有两个参数:

  • thread:
    JavaThread* thread:指向我们当前的线程
  • BasicObjectLock* elem:
    找到BasicObjectLock位于jdk8/hotspot/src/share/vm/runtime/basicLock.hpp
// 一个BasicObjectLock将一个特定的Java对象与一个BasicLock相关联,
// 它目前被嵌入到解释器框架中。
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  // 锁对象,必须双字(一般一个字是4字节)对齐
  BasicLock _lock;
  // 持有该锁的对象                                    
  oop       _obj;                                     
  • 继续看BasicLock,摘录部分代码如下:
class BasicLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  // 该私有变量_displaced_header就是用来描述对象头信息的
  volatile markOop _displaced_header;
  public:
   markOop      displaced_header() const               { return _displaced_header; }
   // 保存对象头的方法
   void         set_displaced_header(markOop header)   { _displaced_header = header; }
}
2.6.3 锁升级

注意前面我们提到的monitorenter方法中核心代码:

if (UseBiasedLocking) {
  // 当开启了偏向锁模式时,就优先使用快速进入模式避免不必要的锁膨胀
  ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
  ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}

也就是说,从这里开始,我们的synchronized就要从偏向锁开启锁升级之旅(前提是开启了偏向锁设置,否则从轻量级锁开始)。

2.7 偏向锁

2.7.1 偏向锁的获取
2.7.1.1 fast_enter
// 快速获取/释放monitor所有权
// 该方法在竞争场景下十分敏感
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 判断是否使用偏向锁
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
    // 不在安全点,调用revoke_and_rebias方法获取偏向锁
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
    // 处于安全点,调用revoke_at_safepoint释放偏向锁
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }
 // 除非不在安全点,获取了偏向锁。其他情况都会走到这一步,采用slow_enter轻量级锁
 slow_enter (obj, lock, THREAD) ;
}
2.7.1.2 biasedLocking.revoke_and_rebias

revoke_and_rebias方法代码位于jdk8/hotspot/src/share/vm/runtime/biasedLocking.cpp,主要做的就是获取偏向锁。确实因为能力有限看不懂了 - - 。这里转载占小狼大神JVM源码分析之synchronized实现一文对该过程的描述,略有改动:

  1. 通过markOop mark = obj->mark()获取对象的对象头的Mark Word
  2. 判断mark是否为可偏向状态,即mark的偏向锁标志位为 1且锁标志位为 01;
  3. 判断markJavaThread指针的状态:
    • 如果为空,则进入步骤(4);
    • 如果指向当前线程,则执行同步代码块;
    • 如果指向其它线程,进入步骤(5);
  4. 通过CAS原子指令设置mark中的JavaThread为当前线程ID。如果执行CAS成功,则执行同步代码块,否则进入步骤(5);
  5. 如果执行CAS失败,表示当前存在多个线程竞争锁。当达到全局安全点(safepoint),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级锁。升级完成后被阻塞在安全点的线程继续执行同步代码块。
2.7.2 偏向锁的释放

前面提到过,当已经位于安全点(is_at_safepoint),才可以释放偏向锁。

2.7.2.1 BiasedLocking::revoke_at_safepoint
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
  //又一次断言,只允许在安全点调用该方法
  assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
  // 封装了线程和锁对象
  oop obj = h_obj();
  // 这一步作用是更新释放偏向锁的计数
  HeuristicsResult heuristics = update_heuristics(obj, false);
  if (heuristics == HR_SINGLE_REVOKE) {
  // 单撤销模式
    revoke_bias(obj, false, false, NULL);
  } else if ((heuristics == HR_BULK_REBIAS) ||
             (heuristics == HR_BULK_REVOKE)) {
    // 批量撤销模式(因为heuristics == HR_BULK_REBIAS为false)
    bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
  }
  // 遍历线程列表,清除所有monitor锁缓存信息
  clean_up_cached_monitor_info();
}
2.7.2.2 BiasedLocking::Condition revoke_bias

核心代码如下:

// 参数1为锁对象和线程组合
// 参数2为是否允许偏向
// 参数3为是否批量模式
// 参数4申请线程
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
  markOop mark = obj->mark();
  if (!mark->has_bias_pattern()) {
    // 非偏向锁模式直接返回无偏向
    return BiasedLocking::NOT_BIASED;
  }
  uint age = mark->age();
  // 该值代表MarkWord最后3bit101
  markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
  // 该值代表MarkWord最后3bit001
  markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
  // 拥有偏向锁的线程
  JavaThread* biased_thread = mark->biased_locker();
  if (biased_thread == NULL) {
  // 此时偏向锁没有指向任何线程,可能是hashcode误差等原因
    if (!allow_rebias) {
    // 不允许偏向,就设MarkWord为非偏向模式(无锁,倒数第三bit设为0,最后2bit 01)
      obj->set_mark(unbiased_prototype);
    }
    // 返回已撤销偏向锁
    return BiasedLocking::BIAS_REVOKED;
  }
  
  bool thread_is_alive = false;
  if (requesting_thread == biased_thread) {
  // 看当前申请偏向锁线程是否就是拥有偏向锁的线程
    thread_is_alive = true;
  } else {
  // 单撤销偏向锁的时候,requesting_thread为null
  // 就遍历所有线程
    for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
      if (cur_thread == biased_thread) {
      // 找到了拥有偏向锁的线程,跳出循环
        thread_is_alive = true;
        break;
      }
    }
  }
  
  if (!thread_is_alive) {
  // 拥有偏向锁的线程已经挂了
    if (allow_rebias) {
    // 允许申请,就直接设为匿名可偏向模式,即MarkWord中线程id设为null,最后2bit 01
      obj->set_mark(biased_prototype);
    } else {
    // 否则设为非偏向模式(无锁,倒数第三bit设为0,最后2bit 01)
      obj->set_mark(unbiased_prototype);
    }
    // 返回偏向锁已经撤销
    return BiasedLocking::BIAS_REVOKED;
  }
  // 来到这里,说明拥有偏向锁的线程还活着
  // 后面一系列代码做的事
  // 先检查该线程是否拥有monitor,如果是就将所需的`displaced headers`写入线程的栈中
  // 否则恢复锁对象的头信息为
  if (allow_rebias) {
      // 设置为匿名偏向状态
      obj->set_mark(biased_prototype);
    } else {
      // 设为无锁
      obj->set_mark(unbiased_prototype);
  }
  // 最后返回锁已经撤销
  return BiasedLocking::BIAS_REVOKED;
}

2.8 轻量级锁

2.8.1 slow_enter-获取锁

摘录核心代码如下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  // 获取对象头的MarkWord部分
  markOop mark = obj->mark();
  // 此时不应该是偏向锁模式
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {
  // is_neutral代表无锁
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    // lock保存该对象头的MarkWord
    lock->set_displaced_header(mark);
    // CAS方式将目标对象的MarkWord替换为lock
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    // CAS方式成功,代表该线程成功获取锁,可以返回执行同步块代码了
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
  // 此时MarkWord为轻量级锁状态,且该锁拥有者就是当前Thread
    // 此时就是轻量级锁的重入情况,可以返回了
    
    lock->set_displaced_header(NULL);
    return;
  }

// 搜了下,下面这段代码因为#if 0的,相当于注释永不运行。。。
#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  // 到了这里,说明轻量级锁存在竞争,需要膨胀为重量级锁
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
} 
2.8.2 slow_exit-释放锁1
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
  fast_exit (object, lock, THREAD) ;
}
2.8.3 fast_exit-释放锁2

摘录核心代码如下:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
    // dhw指向MarkWord副本
    markOop dhw = lock->displaced_header();
    markOop mark ;
    // mark指向真实MarkWord
    mark = object->mark() ;
    if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     // 如果当前线程拥有锁,就CAS方式还原MarkWord
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  // CAS失败,说明其他线程尝试过获取该锁。
  // 此时不仅要释放锁,还需要膨胀为重量级锁
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

2.9 重量级锁-膨胀

实现主要在以下方法中:

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) 

关于重量级锁的膨胀、锁enter/exit等细节待补充,可以参见占小狼的JVM源码分析之synchronized实现

2.10 monitorexit源码分析

2.10.1 InterpreterRuntime::monitorexit

核心代码如下:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
  // 这个h_obj封装了该线程和拥有锁的对象
  Handle h_obj(thread, elem->obj());
  if (elem == NULL || h_obj()->is_unlocked()) {
  // 锁对象关联为空或是已经解锁,就抛出IllegalMonitorStateException
    THROW(vmSymbols::java_lang_IllegalMonitorStateException());
  }
  // 调用slow_exit方法解除该线程的锁拥有权
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  elem->set_obj(NULL);
2.10.2 ObjectSynchronizer::slow_exit
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
  // 直接调用fast模式退出
  fast_exit (object, lock, THREAD) ;
}
2.10.3 ObjectSynchronizer::fast_exit

核心代码如下:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  // if displaced header is null, the previous enter is recursive enter, no-op
  // dhw暂存MrkWord副本
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {
     // Recursive stack-lock.
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     // 对象头的MarkWord
     mark = object->mark() ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
        assert(((oop)(m->object()))->mark() == mark, "invariant") ;
        assert(m->is_entered(THREAD), "invariant") ;
     }
     return ;
  }
}

0x03 synchronized对比ReentrantLock

可重入性等待可中断公平性绑定对象数性能优化
synchronized支持不支持非公平只能1个较多
ReentrantLock支持支持非公平/公平可以多个-

0x04 对象锁和类锁

ObjectMonitor分为对象锁和类锁,控制范围不同,可以查看这篇文章:Java-并发-锁-synchronized之对象锁和类锁

0x05 总结

synchronized在JDK和用户代码中大量使用,JDK作者们以后的优化方向肯定也会是synchronized。因为其底层采用cpp所写,既然现在都能提出那么多锁优化的内容,那可见优化空间还是有的,想必以后synchronized会越来越快。

关于锁优化和锁升级知识的更多详细内容请点击此文:Java-并发-关于锁的一切

0xFF 参考文档

更多好文

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值