ReentrantReadWriteLock源码分析(JDK 1.7)

ReentrantReadWriteLock是ReadWriteLock接口的具体实现类。

ReadWriteLock描述:
ReadWriteLock维护了一组相对应的锁。只读锁和写锁。只要没有写操作,那么只读锁可以同时被多个线程所持有(并发访问共享数据)。写锁是独占锁,同一时刻只允许一个线程持有。

所有的ReadWriteLock实现类一定要保证writeLock操作的内存同步效果。如果一个线程成功的获取到读锁,那么它能看到先前写锁修改的所有数据。
在大量并发下操作共享数据,由于读写锁只有在修改数据时候才是互斥模式,在读时候允许多个线程并发读取。所以读写锁比互斥锁吞吐量高。与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。

相对于互斥锁来说,读写锁是否能够提升性能取决于读数据和写数据的消耗时间比例、读数据和写数据相互竞争的线程数量。举个例子:一个集合在初始化时候被填充好数据,然后频繁读取数据却很少修改数据,这时 读写锁是个很好的选择。然而,如果是修改数据频繁的话,那么大部分时间读写锁都处于互斥模式。所以它几乎是没有性能提升的。更深入一点来说,由于读写锁的实现的内部实现相对互斥锁来说更为复杂,如果使用读写锁在修改数据频繁(读数据占有比例低)的场景中,那么读写锁大部分的消耗时间都是在处理读写锁内部运作的开销了。

虽然读写锁的基本操作是比较简单的,但是在实现类内部实际上必须得作出许多决策。这些决策会影响读写锁在应用里的有效性。
策略例子:
1.当writer(可以理解为执行修改动作的线程)刚释放完写锁,而writers和readers都在等待执行,这时需要决定优先执行读锁还是写锁。通常都是写锁优先级更高,因为在读写锁的使用场景里,通常写锁都是不频繁使用的,且执行时间比较短。而读锁通常优先级靠后,因为它还使写锁需要等待相当长的一段时间才能被运行执行。或者,完全公平也可以。按照入队的先后顺序来决定它们的执行顺序。

2.当读锁处于活动状态时候,而写锁处于等待状态时,决定reader请求的读锁是否被执行。读锁的优先级会被无限期延后,等待写锁执行完毕。

3.决定锁是否可重入:一个写锁的线程是否可以重新获取写锁?当处于写锁时候,是否能获取读锁?读锁能否重入?
4.可以将写锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?

读写锁接口只有两个方法
Lock readLock(); //获取读锁

Lock writeLock();    //获取写锁

下面看看ReentrantReadWriteLock,它是ReentantLock的实现类
ReentrantReadWriteLock的特性:

获取顺序
该类并不强制指定reader或者writer获取相关锁的优先级。它提供一个关于公平策略的可选参数来让用户决定锁的获取优先 级。
非公平模式(默认)
当使用非公平模式构造函数时(默认无参构造是非公平),读锁和写锁的执行顺序是未被指定的,也会受到重入的限制。由于非公平会连续不断的竞争,会导致writer线程和reader线程被无限期的延迟。但是非公平锁的吞吐量要高于公平锁的吞吐量。
公平模式
当使用公平模式的构造函数时,会根据线程大概到达的顺序来决定线程竞争的胜出者。 当前持有的锁被释放时,会优先执行以下两种情况
1.等待最长时间的writer线程将会被分配写锁
2.一组reader线程的等待时间长于所有的writer线程,该组reader线程将会被分配读锁。
当前写锁被持有或者writrer线程正在等待,那么如果一个线程要获取公平读锁(非阻塞)将会被阻塞,直到当前等待时间最长的writer线程获取到写锁且释放写锁完毕。当然,writer线程放弃等待且写锁是处于空闲的话,如果还有reader线程正在队列等待的话,那么则会为这些reader线程分配读锁。
当一个线程尝试获取一个公平写锁时(非重入),如果当前写锁或者读锁处于被持有的话,那么该线程会阻塞直至写锁或读锁被释放。

重入
该锁允许reader和writer已持有锁情况下再次重新获取锁读锁或者写锁,获取方式如ReentrantLock独占锁一样。如果存在writer线程还持有写锁,那么非重入的reader线程 无法获取读锁。writer可以获取到读锁,但是reader不能获取写锁。
reader和writer的重入次数都被限制不得超过65535,超过则会抛出Error.

锁降级
写锁能降级为读锁,但是读锁不能升级为写锁。

锁获取中断

写锁支持Condition,读锁不支持。

在进入ReentrantReadWriteLock源码之前,需要先了解下,关于 位运算的知识。下面我简单讲讲满足看ReentrantReadWriteLock的位运算相关知识。
<< 左移运算,十进制转为二进制后,然后高位左移N位,低位补0。
如: 5 << 2
二进制:
5 = 0000 0000 0000 0000 0000 0000 0000 0101
左移两位 5 = 00 0000 0000 0000 0000 0000 0000 010100
在数学运算上也等于 = 数字M左移N位 = 数字M * 2的N次方

右移运算,十进制转为二进制后,然后低位右移N位,。 右移的规则只记住一点:符号位不变,左边补上符号位
如: 5 << 2
二进制:
5 = 0000 0000 0000 0000 0000 0000 0000 0101
右移两位 5 = 0000 0000 0000 0000 0000 0000 0000 0001
在数学运算上也等于 = 数字M右移N位 = 数字M / 2的N次方

无符号右移,和右移差不多。只是右移,符号位补0.

按位与运算符(&)(抄袭别人的)

参加运算的两个数据,按二进制位进行“与”运算。
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0
例如:3&5 即 0000 0011 & 0000 0101 = 0000 0001 因此,3&5的值得1。

65535的二进制等于1111111111111111,那么只要是小于等于65535的和他进行与运算,都得到该数本身。

两个构造函数
1.无参
2.带有 支持自定义公平和非公平模式参数

由于Sync继承AbstractQueuedSynchronizer ,所以读者真的想深入了解ReentrantReadWriteLock,建议先深入了解 AbstractQueuedSynchronizer 。否则你也只能知其然而不知其所以然。
已经深入了解AQS的读者可以看下面。整个ReentrantReadWriteLock,只需重点关注核心Sync就可以了。

重入读写锁的核心Sync集成了AQS接口。

ReentrantReadWriteLock顾名思义,读写锁。那么它则有读锁和写锁。
说是两个锁,但实际上它只继承了个AQS,然后利用AQS的共享模式和
互斥模式来实现两个锁的语义。

既然是两个锁,那么ReentrantReadWriteLock肯定得知道这两个锁的计数。
计数:计数就是类似重入独占锁里的AQS的state,利用该state来实现控制独占锁。

可是AQS只有一个state啊!!

解决办法:由于state是个32位的int值,因此可以利用位运算把这32位的state分为两个16位的无符号short来分别存储读锁和写锁的计数。
高16位:共享锁的计数
低16位:独占锁的计数

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;



       /*
        下面是 定义 读锁和写锁 计数的一些常量和方法。
         在ReentrantReadWriteLock里,由于涉及到两个锁。一个是读锁 ,一个是写锁。
         而 ReentrantReadWriteLock得核心Sync集成AQS,了解过AQS的都知道,在
         AQS里,都是通过控制state来实现同步语义的。而state只是个int值(或者long),
         因此如果ReentrantReadWriteLock想通过AQS的state来分别得出读锁和写锁的
         计数时,那么只能把该32位的int的state划分为高16位和低16位的无符号short了。
         在ReentrantReadWriteLock里。 state的低16位代表互斥锁(写锁)的计数。高16位
        代表共享锁(读锁)的计数。
       */


        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** 传入AQS的state,获取该state的高16位。也就是 读锁 的计数  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 传入AQS的state,获取该state的低16位。也就是 写锁 的计数  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }


        /**
        为每个线程记录读锁的重入次数。该内部类的实例由ThreadLocalHoldCounter来维护。
        **/
        static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            //为了避免GC垃圾收集时候,还存在无意义引用,导致GC无效,所以使用线程的ID,而不是线程的引用。
            final long tid = Thread.currentThread().getId();
        }

        /**
         * ThreadLocal subclass. Easiest to explicitly define for sake
         * of deserialization mechanics.
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }


        /**    
        可以通过该成员变量获取当前线程的读锁的重入次数。该成员变量只会在构造函数或者反序列化
        时(readObject方法)被初始化。当读锁的重入次数降到0时候,那么会从该成员变量中删除
        当前线程的HoldCounter实例
        **/
        private transient ThreadLocalHoldCounter readHolds;


        /**
        cachedHoldCounter主要是用来存储上最后一个(可能是上一个)成功获取读锁的线程。
        它的主要作用是用来减少从ThreadLocl.get方法获取HoldCounter ,经过和别人讨论,
        这也许是作者对ReentrantReadWriteLock的一些细节优化吧。

        **/
        private transient HoldCounter cachedHoldCounter;


        /**
        firstReader 代表第一个获取读锁的线程(这里的第一个指的是:可能是首次获取读锁线程,可能是在写锁释放后的首次获取的读锁线程。
        更确切的说,指的是第一个把读锁的计数(shared count)从0改为1的线程。      )。
        firstReaderHoldCount 是firstReader 线程的重入次数
        除非线程没有释放读锁(tryReleaseShared时候会set firstReader =null),否则该行为不会引起垃圾滞留。
        **/
        private transient Thread firstReader = null;
        private transient int firstReaderHoldCount;

        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }

        /*
         * Acquires and releases use the same code for fair and
         * nonfair locks, but differ in whether/how they allow barging
         * when queues are non-empty.
         */

        /**
         * Returns true if the current thread, when trying to acquire
         * the read lock, and otherwise eligible to do so, should block
         * because of policy for overtaking other waiting threads.
         */
        abstract boolean readerShouldBlock();

        /**
         * Returns true if the current thread, when trying to acquire
         * the write lock, and otherwise eligible to do so, should block
         * because of policy for overtaking other waiting threads.
         */
        abstract boolean writerShouldBlock();


        /**
        由于ReentrantReadWriteLock支持Condition,而Condition的wait会调用
        tryRelease 释放AQS的state(该state分为高低16位,包括读锁和写锁的计数)。
        还会调用tryAcquire来重新设置之前tryRelease 的state(具体细节可以参考我的AQS源码分析)
        **/


        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            /**
            注意这是独占模式下的释放方法。  
            boolean free = exclusiveCount(nextc) == 0;
            获取写锁的state(低16位),然后判断state是否==0。是则认为写锁完全释放完成。
            **/
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

        protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            //这里先判断AQS的state是否等于0,如果不等于0,则有可能当前时间读锁或者写锁被持有
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                /**
                这里为什么判断写锁的计数等于0,则返回false呢???
                注意上面的先行条件,c!=0。如果不等于0,则有可能当前时间读锁或者写锁被持有。那么
                w==0则代表当前持有的是读锁。所以理所当然需要返回false。读锁处于活动中,写锁退让等待。
                或者!!
                如果当前持有的是写锁,但是非当前线程的话,那么也可以返回false,毕竟修改数据只能同时允许一个线程的存在.
                **/
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //判断写锁重入次数是否大于65535
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            /**
            writerShouldBlock用于判断writer线程和reader线程同时等待情况时,应该优先获取哪个?
            公平锁和非公平锁对writerShouldBlock都进行了重写。
            公平锁:判断AQS同步等待队列的头结点是否有正在等待的线程,且等待线程非当前线程。如果存在,那么返回false,顺序等待执行。
            非公平锁:直接返回false,以竞争的形式来争夺锁获取
            **/
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;    
                //可以注意到cachedHoldCounter仅仅只是为了减少从readHolds.get();。但是我看了
                //ThreadLocal的源码,get方法消耗并不算大,也许这是作者对该锁的一些细节优化,使其尽可能的快。
                if (rh == null || rh.tid != current.getId())
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                //因为使用state的高16位为读锁的state,所以在获取或者释放时候,都是操作SHARED_UNIT,而不是unused(1)
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

        private IllegalMonitorStateException unmatchedUnlockException() {
            return new IllegalMonitorStateException(
                "attempt to unlock read lock, not locked by current thread");
        }

        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            //判断当前写锁是否被持有 且 持有写锁的线程不是当前线程。则返回-1排队等待
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            /**
            readerShouldBlock是在读锁lock时候,需要首先判断是否要阻塞当前读锁的获取。
            readerShouldBlock有两种实现:
            公平锁:判断AQS同步等待队列的头结点是否有正在等待的线程,且等待线程非当前线程。如果存在,那么返回true,!true=false,顺序排队等待执行。
            非公平锁:判断当前头节点线程是否是writer(互斥模式),如果是,则返回true,!true=false,顺序排队等待执行
            **/
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

        /**
         * Full version of acquire for reads, that handles CAS misses
         * and reentrant reads not dealt with in tryAcquireShared.
         */
        final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != current.getId()) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId())
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }


        /**
       tryWriteLock方法是由写锁的tryLock调用的。该方法和tryAcquire 功能几乎相同,仅仅只是去掉了调用writerShouldBlock的"效果"
        **/
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }


        /**
        tryReadLock方法是由读锁的tryLock调用的。该方法和tryAcquireShared 功能几乎相同,仅仅只是去掉了调用readerShouldBlock的"效果"
        **/
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId())
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        // Methods relayed to outer class

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        final Thread getOwner() {
            // Must read state before owner to ensure memory consistency
            return ((exclusiveCount(getState()) == 0) ?
                    null :
                    getExclusiveOwnerThread());
        }
        //获取读锁被获取的总次数
        final int getReadLockCount() {
            return sharedCount(getState());
        }

        final boolean isWriteLocked() {
            return exclusiveCount(getState()) != 0;
        }

        final int getWriteHoldCount() {
            return isHeldExclusively() ? exclusiveCount(getState()) : 0;
        }
        //获取当前线程的重入次数
        final int getReadHoldCount() {
            if (getReadLockCount() == 0)
                return 0;

            Thread current = Thread.currentThread();
            if (firstReader == current)
                return firstReaderHoldCount;

            HoldCounter rh = cachedHoldCounter;
            if (rh != null && rh.tid == current.getId())
                return rh.count;

            int count = readHolds.get().count;
            //因为当ThreadLocal.get时候,如果不存在初始化对象,则会调用initialValue初始化,所以如果==0,要删除
            if (count == 0) readHolds.remove();
            return count;
        }

        /**
         * Reconstitute this lock instance from a stream
         * @param s the stream
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            readHolds = new ThreadLocalHoldCounter();
            setState(0); // reset to unlocked state
        }

        final int getCount() { return getState(); }
    }
ReentrantReadWriteLock是Java中的一个锁实现,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它是可重入的,也就是说,同一个线程可以多次获取读锁或写锁,而不会出现死锁。 ReentrantReadWriteLock源码实现比较复杂,主要包括两个内部类:Sync和FairSync。Sync是ReentrantReadWriteLock的核心实现,它维护了读锁和写锁的状态,以及等待队列。FairSync是Sync的子类,它实现了公平锁的逻辑。 ReentrantReadWriteLock读写锁是通过Sync类中的state字段实现的。state的高16位表示读锁的持有数量,低16位表示写锁的持有数量。当一个线程获取读锁时,它会增加state的高16位;当一个线程获取写锁时,它会增加state的低16位。当一个线程释放读锁或写锁时,它会减少相应的state值。 ReentrantReadWriteLock的等待队列是通过Sync类中的readQueues和writeQueue字段实现的。当一个线程请求读锁或写锁时,它会被加入到相应的等待队列中。当一个线程释放读锁或写锁时,它会唤醒等待队列中的一个线程。 ReentrantReadWriteLock的公平锁实现是通过FairSync类中的tryAcquireShared和tryAcquire方法实现的。tryAcquireShared方法用于获取读锁,它会先检查等待队列中是否有写锁请求,如果有,则返回失败;否则,它会尝试获取读锁。tryAcquire方法用于获取写锁,它会先检查等待队列中是否有读锁或写锁请求,如果有,则返回失败;否则,它会尝试获取写锁。 总的来说,ReentrantReadWriteLock是一个非常实用的锁实现,它可以提高多线程程序的并发性能。但是,由于它的实现比较复杂,使用时需要注意避免死锁和竞争条件等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值