ReentrantLock 原理


ReentrantLock 主要是通过 AQS + CAS 的方式实现加锁和解锁原理。
这里先说一下重要的几个属性。

  • state 标记是否加锁成功或是否发生锁重入
  • exclusiveOwnerThread 当前拥有锁的线程
  • waitStatus 线程等待状态
变量含义
0当一个 Node 被初始化的时候的默认值
CANCELLED1,表示线程取消获取锁
SIGNAL-1,表示线程已经准备好,等待获取锁了
CONDITION-2,表示线程在等待队列中,等待被唤醒
PROPAGATE-3,线程处在 SHARED 情况下,该字段才会使用

ReentrantLock 有公平锁和非公平锁的实现,默认是非公平锁,可以通过构造方法来决定公平还是非公平。

// 默认构造器,非公平锁
public ReentrantLock() {
   sync = new NonfairSync();
}

// 传入true 为公平锁,false 为非公平锁
public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁实现原理

加锁流程

lock() 方法

final void lock() {
	// CAS 尝试将 0 -> 1
    if (compareAndSetState(0, 1))
    	// CAS 成功,当前线程加锁成功
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	// CAS 失败
        acquire(1);
}

线程首先尝试通过 CAS 将 0->1 ,CAS 成功则当前线程加锁成功,否则加锁失败,调用 acquire() 方法

acquire() 方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里首先分析一下 tryAcquire() 方法,最终调用的是 nonfairTryAcquire() 方法

nonfairTryAcquire() 方法

final boolean nonfairTryAcquire(int acquires) {
	// 这里传入的 acquires == 1
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取 state 值
    int c = getState();
    // state == 0,表示还未加锁
    if (c == 0) {
    	// 尝试 CAS 将 0 -> 1 
        if (compareAndSetState(0, acquires)) {
        	// CAS 成功,当前线程加锁成功
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 拥有锁的线程就是当前线程,说明发生了 锁重入
    else if (current == getExclusiveOwnerThread()) {
    	// 锁重入计数加 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新 state 值,锁重入计数加 1
        setState(nextc);
        return true;
    }
    return false;
}

在 nonfairTryAcquire() 方法中,线程会去尝试加锁,该方法有四个可能的结果。

  • state == 0 时
    • CAS 成功,加锁成功
    • CAS 失败,加锁失败
  • state != 0
    • 当前线程之前已经获取到锁了,发生锁重入,计数加1,state 值加1
    • 锁已经被其他线程获取到了,加锁失败

再来分析一下 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

addWaiter()

private Node addWaiter(Node mode) {
	// 将当前线程包装为一个 Node 节点,并设置为独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // 获取尾结点
    Node pred = tail;
    // 尾结点不为 null
    if (pred != null) {
    	// 设置尾结点为当前节点的头结点
        node.prev = pred;
        // 尝试 CAS 将当前节点添加到队列尾部 
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 该方法作用是将当前节点加入队列尾部
    enq(node);
    return node;
}
private Node enq(final Node node) {
	// 无限循环尝试 CAS 将当前节点添加到队列尾部,直到添加成功才退出循环
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter() 方法的主要作用是将当前线程包装成 Node 节点并添加到队列尾部。

acquireQueued() 方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    	// 打断标记
        boolean interrupted = false;
        for (;;) {
        	// 获取当前线程的前置节点
            final Node p = node.predecessor();
            // 如果前置节点是头结点则再次调用 tryAcquire() 方法去加锁
            // 只有前置节点是头结点,当前线程才有机会去获取锁
            if (p == head && tryAcquire(arg)) {
            	// 当前线程加锁成功
            	// 设置当前节点为头结点
                setHead(node);
                // 垃圾回收前置节点
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断当前线程在获取锁失败之后是否应该等待
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// 线程进入等待并检查一下中断标记
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    	// 线程有中断标记
        if (failed)
        	// 线程被打断,不再去获取锁
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire() 方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取前一个线程节点的状态
    int ws = pred.waitStatus;
    
    // 前一个线程还处于阻塞状态,未获取到锁
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
         // 返回 true,表示当前阻塞当前线程
        return true;
        
    // 表示前一个节点已经取消获取锁了
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
         // 找到所有取消获取锁的线程,并将他们从等待队列中删除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         // 给前置节点设置标记为 Node.SIGNAL,这次先不将当前线程阻塞,以便于下次进入该方法的时候,将当前线程阻塞
         // 这样做的好处是又可以去尝试获取一次锁
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回 false,表示不阻塞当前线程
    return false;
}

parkAndCheckInterrupt() 方法

private final boolean parkAndCheckInterrupt() {
	// 阻塞当前线程
    LockSupport.park(this);
    // 返回当前线程是否被打断
    return Thread.interrupted();
}

acquireQueued() 方法中有两个主要作用,一是去阻塞当前线程,二是还会去尝试让当前线程去获取锁。

阻塞队列中能获取锁的线程只有队列中的第二个节点线程(当前节点的前置节点是头结点)。

解锁流程

unlock() 方法

public void unlock() {
  sync.release(1);
}
public final boolean release(int arg) {
	// 尝试释放锁
    if (tryRelease(arg)) {
    	// 获取头结点
        Node h = head;
        // 头结点不为 null 且状态不为 0
        if (h != null && h.waitStatus != 0)
        	// 唤醒队列中的一个线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease() 方法

protected final boolean tryRelease(int releases) {
	// 将 state 值减 1
    int c = getState() - releases;
    // 当前线程并不是获取到锁的线程,抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 标记锁是否成功释放
    boolean free = false;
    // 值为0,表示锁已经成功释放
    // 值不为0,表示之前发生了锁重入,state 计数减 1
    if (c == 0) {
    	// 成功释放锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新 state 值
    setState(c);
    return free;
}

tryRelease() 方法的主要作用有两个。

  • state == 1 时,成功解锁
  • state > 1 时,说明发生了锁重入,state 值减 1

unparkSuccessor() 方法

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    // 获取当前线程节点的状态(当前节点是头结点)
    int ws = node.waitStatus;
    // 当前线程节点状态不是 CANCELLED 状态
    if (ws < 0)
    	// 尝试 CAS 将当前线程节点状态设置为 0
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     // 获取头结点的后置节点
    Node s = node.next;
    // 后置节点为 null 或者 后置节点状态为 CANCELLED
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前遍历等待队列,找到离头结点最近并且状态不为 CANCELLED 的一个节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
    	// 唤醒该线程
        LockSupport.unpark(s.thread);
}

unparkSuccessor() 方法的主要作用是唤醒等待队列中离头结点最近的一个线程。

不可打断模式

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    	// 打断标记
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 需要获取锁成功之后,才能返回这个打断标记
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// 检查线程的打断标记
                parkAndCheckInterrupt())// 设置打断标记为 true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    // 返回并清除线程的打断标记
    return Thread.interrupted();
}

可以看到,在不可打断模式下,仅仅只是设置了一下线程的打断标记,并没有实际性的打断操作。

可打断模式

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加锁失败
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// 检查打断标记,如果线程被 Thread.interrupt() 方法打断,返回true
                parkAndCheckInterrupt())
                // 线程被打断,抛出异常,结束 for(;;) 循环
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在可打断模式下,线程如果被 Thread.interrupt() 方法设置了打断标记,则会抛出异常,线程被中断,停止去加锁

公平锁实现原理

加锁流程

tryAcquire() 方法

protected final boolean tryAcquire(int acquires) {
	// 这里传入的 acquires == 1
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取 state 值
    int c = getState();
    // state == 0,表示还未加锁
    if (c == 0) {
    	
        if (
        	// 判断是否有前驱结点
        	!hasQueuedPredecessors() &&
        	// 尝试 CAS 将 0 -> 1 
            compareAndSetState(0, acquires)) {
            // CAS 成功,当前线程加锁成功
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 拥有锁的线程就是当前线程,说明发生了 锁重入
    else if (current == getExclusiveOwnerThread()) {
    	// 锁重入计数加 1
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 更新 state 值,锁重入计数加 1
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors() 方法

public final boolean hasQueuedPredecessors() {
    // 尾结点
    Node t = tail; 
    // 头结点
    Node h = head;
    Node s;
    // h != t 队列中有在排队的线程结点
    // (s = h.next) == null 只有头结点
    // s.thread != Thread.currentThread() 头结点的下一个结点不是当前结点
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

公平锁相比较于非公平锁的实现方式就是多了一个 hasQueuedPredecessors() 方法的判断。这个方法是判断等待队列中是否还有其他线程排在当前结点前面。意思是,这个队列是一个先进先出的队列,排在前面的线程优先获取到锁。

解锁流程

和非公平锁的解锁流程相同

条件变量 Condition

调用 Condition.await() 方法实际上调用的是其实现类 ConditionObject.await()方法

我们先看下 ConditionObject

/** First node of condition queue. */
/** 等待队列中的第一个结点 */
private transient Node firstWaiter;

/** Last node of condition queue. */
/** 等待队列中的最后一个结点 */
private transient Node lastWaiter;

await() 方法

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程结点添加到 ConditionWaiter 队列中
    Node node = addConditionWaiter();
    // "完全"释放线程持有的锁
    // 因为线程可能发生重入,所有这里叫完全释放
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    // 当前线程结点没有添加到 AQS 阻塞队列中
    while (!isOnSyncQueue(node)) {
    	// 阻塞该线程
        LockSupport.park(this);
        // 被打断,退出循环
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 尝试去获取锁,并且线程打断模式是不是 THROW_IE(抛出异常,可打断模式)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    	// 设置线程打断状态为 REINTERRUPT(不可打断模式)
        interruptMode = REINTERRUPT;
    // 清除队列中已取消等待的线程结点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // interruptMode != 0 说明了有打断标记
    if (interruptMode != 0)
    	// 根据打断模式做出相应处理
        reportInterruptAfterWait(interruptMode);
}

addConditionWaiter() 方法

private Node addConditionWaiter() {
	// 获取尾结点
    Node t = lastWaiter;
    // 尾结点状态不是 CONDITION 状态
    if (t != null && t.waitStatus != Node.CONDITION) {
    	// 清除队列中所有取消等待的线程结点
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
	// 构造当前线程结点,状态为 CONDITION 
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 将结点入队
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

fullyRelease() 方法

final long fullyRelease(Node node) {
    boolean failed = true;
    try {
        long savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

isOnSyncQueue() 方法

// 判断线程结点是否在 AQS 队列中
final boolean isOnSyncQueue(Node node) {
	 // 没有在 AQS 队列中
     if (node.waitStatus == Node.CONDITION || node.prev == null)
         return false;
     // 已经在 AQS 队列中
     if (node.next != null) // If has successor, it must be on queue
         return true;
     /*
      * node.prev can be non-null, but not yet on queue because
      * the CAS to place it on queue can fail. So we have to
      * traverse from tail to make sure it actually made it.  It
      * will always be near the tail in calls to this method, and
      * unless the CAS failed (which is unlikely), it will be
      * there, so we hardly ever traverse much.
      */
      // 从后向前遍历 AQS 队列,查看该线程结点是否在 AQS 队列中
     return findNodeFromTail(node);
 }
// 从后向前遍历 AQS 队列,查看该线程结点是否在 AQS 队列中 
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

checkInterruptWhileWaiting() 方法

private int checkInterruptWhileWaiting(Node node) {
   return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

transferAfterCancelledWait() 方法

// 转换结点状态,等待状态 -> 初始状态
// 如果被设置取消唤醒的结点还没有被唤醒的话,返回 true
final boolean transferAfterCancelledWait(Node node) {
	// CAS 将结点状态由 等待状态 -> 初始状态
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    /*
     * If we lost out to a signal(), then we can't proceed
     * until it finishes its enq().  Cancelling during an
     * incomplete transfer is both rare and transient, so just
     * spin.
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

reportInterruptAfterWait() 方法

// 根据打断模式,做相应行为
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    // 打断模式为可打断模式
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    // 打断模式为不可打断模式
    else if (interruptMode == REINTERRUPT)
    	// 设置一个打断标记
        selfInterrupt();
}

ConditionObject.await() 方法的功能是将线程结点加入 ConditionWaiter 队列,并调用 LockSupport.part() 方法暂停线程

signal() 方法

public final void signal() {
	// 当前线程是否持有锁
	// 如果当前线程不是持有锁的线程将会抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取 ConditionWaiter 队列中的第一个元素
    Node first = firstWaiter;
    if (first != null)
    	// 唤醒线程
        doSignal(first);
}

isHeldExclusively() 方法

protected final boolean isHeldExclusively() {
    // 当前线程是否持有锁
    return getExclusiveOwnerThread() == Thread.currentThread();
}

doSignal() 方法

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
    /*
     * 改变线程状态,如果失败,说明线程结点已经取消被唤醒了
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
        
    // 将线程结点加入到 AQS 队列中,并返回它的前置结点
    Node p = enq(node);
    int ws = p.waitStatus;
    // ws > 0 前置结点已经取消被唤醒了
    // 设置前置结点状态为 SIGNAL
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ReentrantLockJava中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLockJava中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLockJava中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值