LongAdd

目录

 为什么要用LongAddr?

 LongAddr里面的内容

Cell累加单元

 共享

 如何防止伪共享?

LongAddr累加方法:Cell累加和base累加

 线程累加值到哪个cell中?

Unsafe

简单阐述Unsafe的作用

2.Unsafe中的CAS操作

3.Unsafe原子操作介绍

4.Unsafe中线程调度的相关方法

5.例子


 为什么要用LongAddr?

之前我们所学的AtomicLong本质是通过CAS方法来保证原子性的,但是如果多线程下

,一个线程t1改变了共享资源状态,而另一线程t2因为资源改变,导致CAS失败,那么t2就会不断去自旋进行多次尝试——>导致性能底下;

场景:

在高并发情况下,大量线程去操作同一共享资源,那么势必会造成大多数线程CAS失败,然后就会陷入自旋状态,那么就会浪费CPU资源,并且降低了并发性(:一时间段发生事件的次数);

解决:

我们试想,如果将一个变量拆分为多个变量,是不是就是多个线程竞争多个变量了,这样不就增强并发性了,并且还会减少CPU资源浪费;


 LongAddr里面的内容

 LongAddr内部实际上是维护了一个Cells数组,里面有很多Cell,每个Cell里面都有一个初始化为0的Long值;在多线程进行竞争的时候,会对cell进行争取,当线程竞争失败的时候,它不会立即进行自旋,而是去竞争其他的cell——>好处:减少CPU消耗;

当线程竞争到cell,获取的值是base+当前变量值;

Cells的创建:

Cells占用内存比较大,所以它一开始不会创建,而是需要的时候创建(懒加载);

base:基础值

类似于AtomicInteger中的value,在没有竞争的情况下不会使用cells数组,而是使用base做CAS累加;

cellsBusy:相当于加锁的标记

比如Cells数组创建,扩容

cellsBusy作用是当修改cells数组时,起到一个加锁的作用,防止多线程同时修改cells数组:

0为无锁,1为加锁,加锁状况有三种:

1.cells数组初始化的时候;

2.cell数组扩容的时候;

3.cells中如果某个元素为null,给这个位置创建新的Cell对象的时候;

有竞争后cells的变化:

第一次初始化,cells容度为2,每次扩容都是以前的2倍,直到cells数组长度>=当前服务器CPU的数量就不再扩容(CPU能够并行的执行CAS操作的最大数量就是它的核数),每个线程会通过Cell对象中的value进行累加,相当于将线程绑定到cell对象上;

每个线程在竞争得到数之后,会将累加的数+Cells数组中的竞争到的cell元素中——>sum=base+[0-n]cells;

注:

一旦对base的CAS进行操作失败(多线程状态竞争),那么就会初始化cells数组;


 striped64类

Striped64是一个高并发累加的工具类。
Striped64的设计核心思路:就是通过内部的分散计算来避免竞争。 
Striped64内部包含一个base和一个Cell[] cells数组,又叫hash表。 
累加方式:没有竞争的情况下,要累加的数通过cas累加到base上;如果有竞争的话,会将要累加的数累加到Cells数组中的某个cell元素里面。所以整个Striped64的值为sum=base+∑[0~n]cells。

Striped64核心属性
transient volatile Cell[] cells;  // 存放cell的hash表,大小为2乘幂
transient volatile long base;  // 基础值(1.无竞争时更新,2.cells数组初始化过程不可用时,也会通过cas累加到base)
transient volatile int cellsBusy;  // 自旋锁,通过CAS操作加锁(1.初始化cells数组,2.创建cell单元,3.cells扩容) 

Cell累加单元

​ // 为提高性能,使用注解@sun.misc.Contended,用来避免伪共享
 // 伪共享简单来说就是会破坏其它线程在缓存行中的值,导致重新从主内存读取,降低性能。
 @sun.misc.Contended static final class Cell {
        //用来保存要累加的值
        volatile long value;
        Cell(long x) { value = x; }
        //使用UNSAFE类的cas来更新value值
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
        private static final sun.misc.Unsafe UNSAFE;
        //value在Cell类中存储位置的偏移量;
        private static final long valueOffset;
        //这个静态方法用于获取偏移量
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

 共享

主频Hz越高,耗时越少;

 很重要的一句话:CPU要保证数据的一致性,如果某个CPU更改了数据,那么缓存必然全部作废,需要从内存中重新读取最新数据;

一个缓存行是64bit

 注意:Cell是数组形式的,所以在内存中是连续储存的,一个Cell为24bit,对象头(markWord+KlassWord)有24bit,还有8bit的value;——>所以说缓存行能够放下2个Cell对象;

当一个CPU修改了缓存行内容

另一个CPU缓存直接废,因为要保证数据同步;

 如何防止伪共享?

一个缓存行有多个Cell就是伪共享

 @sun.misc.Contented作用:一个缓存行中只能有一个Cell


LongAddr累加方法:Cell累加和base累加

add:就是LongAddr累加的方法,x为累加值 

longAccumulate:一个为累加值,一个为null,另一个为是否发生竞争

// 累加方法,参数x为累加的值    
public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        /**
         * 如果一下两种条件则继续执行if内的语句
         * 1. cells数组不为null(不存在争用的时候,cells数组一定为null,一旦对base的cas操作失败,才会初始化cells数组)
         * 2. 如果cells数组为null,如果casBase执行成功,则直接返回,如果casBase方法执行失败(casBase失败,说明第一次争用冲突产生,需要对cells数组初始化)进入if内;
         * casBase方法很简单,就是通过UNSAFE类的cas设置成员变量base的值为base+要累加的值
         * casBase执行成功的前提是无竞争,这时候cells数组还没有用到为null,可见在无竞争的情况下是类似于AtomticInteger处理方式,使用cas做累加。
         */
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            //uncontended判断cells数组中,当前线程要做cas累加操作的某个元素是否#不#存在争用,如果cas失败则存在争用;uncontended=false代表存在争用,uncontended=true代表不存在争用。
            boolean uncontended = true;
            /**
            *1. as == null : cells数组未被初始化,成立则直接进入if执行cell初始化
            *2. (m = as.length - 1) < 0: cells数组的长度为0
            *条件1与2都代表cells数组没有被初始化成功,初始化成功的cells数组长度为2;
            *3. (a = as[getProbe() & m]) == null :如果cells被初始化,且它的长度不为0,则通过getProbe方法获取当前线程Thread的threadLocalRandomProbe变量的值,初始为0,然后执行threadLocalRandomProbe&(cells.length-1 ),相当于m%cells.length;如果cells[threadLocalRandomProbe%cells.length]的位置为null,这说明这个位置从来没有线程做过累加,需要进入if继续执行,在这个位置创建一个新的Cell对象;
            *4. !(uncontended = a.cas(v = a.value, v + x)):尝试对cells[threadLocalRandomProbe%cells.length]位置的Cell对象中的value值做累加操作,并返回操作结果,如果失败了则进入if,重新计算一个threadLocalRandomProbe;
            如果进入if语句执行longAccumulate方法,有三种情况
            1. 前两个条件代表cells没有初始化,
            2. 第三个条件指当前线程hash到的cells数组中的位置还没有其它线程做过累加操作,
            3. 第四个条件代表产生了冲突,uncontended=false
            **/
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }
 
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        //获取当前线程的threadLocalRandomProbe值作为hash值,如果当前线程的threadLocalRandomProbe为0,说明当前线程是第一次进入该方法,则强制设置线程的threadLocalRandomProbe为ThreadLocalRandom类的成员静态私有变量probeGenerator的值,后面会详细将hash值的生成;
        //另外需要注意,如果threadLocalRandomProbe=0,代表新的线程开始参与cell争用的情况
        //1.当前线程之前还没有参与过cells争用(也许cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),是第一次执行base的cas累加操作失败;
        //2.或者是在执行add方法时,对cells某个位置的Cell的cas操作第一次失败,则将wasUncontended设置为false,那么这里会将其重新置为true;第一次执行操作失败;
       //凡是参与了cell争用操作的线程threadLocalRandomProbe都不为0;
        int h;
        if ((h = getProbe()) == 0) {
            //初始化ThreadLocalRandom;
            ThreadLocalRandom.current(); // force initialization
            //将h设置为0x9e3779b9
            h = getProbe();
            //设置未竞争标记为true
            wasUncontended = true;
        }
        //cas冲突标志,表示当前线程hash到的Cells数组的位置,做cas累加操作时与其它线程发生了冲突,cas失败;collide=true代表有冲突,collide=false代表无冲突 
        boolean collide = false; 
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            //这个主干if有三个分支
            //1.主分支一:处理cells数组已经正常初始化了的情况(这个if分支处理add方法的四个条件中的3和4)
            //2.主分支二:处理cells数组没有初始化或者长度为0的情况;(这个分支处理add方法的四个条件中的1和2)
            //3.主分支三:处理如果cell数组没有初始化,并且其它线程正在执行对cells数组初始化的操作,及cellbusy=1;则尝试将累加值通过cas累加到base上
            //先看主分支一
            if ((as = cells) != null && (n = as.length) > 0) {
                /**
                 *内部小分支一:这个是处理add方法内部if分支的条件3:如果被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用,则用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁,然后将这个Cell对象放到cells[m%cells.length]位置上 
                 */
                if ((a = as[(n - 1) & h]) == null) {
                    //cellsBusy == 0 代表当前没有线程cells数组做修改
                    if (cellsBusy == 0) {
                        //将要累加的x值作为初始值创建一个新的Cell对象,
                        Cell r = new Cell(x); 
                        //如果cellsBusy=0无锁,则通过cas将cellsBusy设置为1加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            //标记Cell是否创建成功并放入到cells数组被hash的位置上
                            boolean created = false;
                            try {
                                Cell[] rs; int m, j;
                                //再次检查cells数组不为null,且长度不为空,且hash到的位置的Cell为null
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    //将新的cell设置到该位置
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                //去掉锁
                                cellsBusy = 0;
                            }
                            //生成成功,跳出循环
                            if (created)
                                break;
                            //如果created为false,说明上面指定的cells数组的位置cells[m%cells.length]已经有其它线程设置了cell了,继续执行循环。
                            continue;
                        }
                    }
                   //如果执行的当前行,代表cellsBusy=1,有线程正在更改cells数组,代表产生了冲突,将collide设置为false
                    collide = false;
 
                /**
                 *内部小分支二:如果add方法中条件4的通过cas设置cells[m%cells.length]位置的Cell对象中的value值设置为v+x失败,说明已经发生竞争,将wasUncontended设置为true,跳出内部的if判断,最后重新计算一个新的probe,然后重新执行循环;
                 */
                } else if (!wasUncontended)  
                    //设置未竞争标志位true,继续执行,后面会算一个新的probe值,然后重新执行循环。 
                    wasUncontended = true;
                /**
                *内部小分支三:新的争用线程参与争用的情况:处理刚进入当前方法时threadLocalRandomProbe=0的情况,也就是当前线程第一次参与cell争用的cas失败,这里会尝试将x值加到cells[m%cells.length]的value ,如果成功直接退出  
                */
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                /**
                 *内部小分支四:分支3处理新的线程争用执行失败了,这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false,后面重新计算prob的值*/
                else if (n >= NCPU || cells != as)
                    collide = false;
                /**
                 *内部小分支五:如果发生了冲突collide=false,则设置其为true;会在最后重新计算hash值后,进入下一次for循环
                 */
                else if (!collide)
                    //设置冲突标志,表示发生了冲突,需要再次生成hash,重试。 如果下次重试任然走到了改分支此时collide=true,!collide条件不成立,则走后一个分支
                    collide = true;
                /**
                 *内部小分支六:扩容cells数组,新参与cell争用的线程两次均失败,且符合库容条件,会执行该分支
                 */
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        //检查cells是否已经被扩容
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //为当前线程重新计算hash值
                h = advanceProbe(h);
 
            //这个大的分支处理add方法中的条件1与条件2成立的情况,如果cell表还未初始化或者长度为0,先尝试获取cellsBusy锁。
            }else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {  // Initialize table
                    //初始化cells数组,初始容量为2,并将x值通过hash&1,放到0个或第1个位置上
                    if (cells == as) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    //解锁
                    cellsBusy = 0;
                }
                //如果init为true说明初始化成功,跳出循环
                if (init)
                    break;
            }
            /**
             *如果以上操作都失败了,则尝试将值累加到base上;
             */
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) // Fall back on using base
                break;  
        }
    }

 线程累加值到哪个cell中?

需要考虑hash生成策略

hash就是Thread类中的的一个成员变量,初始值为0;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr") 
int threadLocalRandomProbe;
 
// LongAdder的父类Striped64里通过getProbe方法获取当前线程threadLocalRandomProbe
static final int getProbe() {
    // PROBE是threadLocalRandomProbe变量在Thread类里面的偏移量
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
 
// threadLocalRandomProbe初始化
// 线程对LongAdder的累加操作,在没有进入longAccumulate方法前,threadLocalRandomProbe一直都是0,当发生争用后才会进入longAccumulate方法中,进入该方法第一件事就是判断threadLocalRandomProbe是否为0,如果为0,则将其设置为0x9e3779b9
int h;
if ((h = getProbe()) == 0) {
   ThreadLocalRandom.current(); 
   h = getProbe();
   //设置未竞争标记为true
   wasUncontended = true;
}
 
static final void localInit() {
   // private static final AtomicInteger probeGenerator = new AtomicInteger();
   // private static final int PROBE_INCREMENT = 0x9e3779b9;
   int p = probeGenerator.addAndGet(PROBE_INCREMENT);
   int probe = (p == 0) ? 1 : p; // skip 0
   long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
   Thread t = Thread.currentThread();
   UNSAFE.putLong(t, SEED, seed);
   UNSAFE.putInt(t, PROBE, probe);
}
 
threadLocalRandomProbe重新生成
static final int advanceProbe(int probe) {
   probe ^= probe << 13;   // xorshift
   probe ^= probe >>> 17;
   probe ^= probe << 5;
   UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
   return probe;
}

Unsafe

因为是私有的,如果我们想获取这个对象的话,需要通过反射获取;

简单阐述Unsafe的作用

Unsafe可以帮助我直接操作硬件资源,但是官方不推荐;

获得:基于反射获取Unsafe

//1.最简单的使用方式是基于反射获取Unsafe实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
package com.example.juc.CAS;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @author diao 2022/4/18
 */
public class UnsafeAccessor {
    private static Unsafe unsafe;
    
    static{
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe= (Unsafe) theUnsafe.get(null); 
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    } 
    
    public static Unsafe getUnsafe(){
        return unsafe;
    }
}

2.Unsafe中的CAS操作

/**
 * CAS 操作
 *
 * @param o        包含要修改field的对象
 * @param offset   对象中某field的偏移量
 * @param expected 期望值
 * @param update   更新值
 * @return true | false
 */
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

思路:执行要修改的变量对象,判断当前位置上的值是否与期待值相等(当前值:->由偏移量offset确定),如果相等就将该位置上的值更新为最新值;

深入:CAS其实是一条CPU指令(cmpxchg指令),不会造成所谓数据不一致问题;

而在执行CPU指令时,会先判断当前系统是否为多核系统,是则会给总线程加锁,再执行CAS操作;

3.Unsafe原子操作介绍

/**
 * int类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
 *
 * @param var1 操作的对象
 * @param var2 var2字段内存地址偏移量
 * @param var4 需要加的值
 * @return
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

/**
 * long类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
 *
 * @param var1 操作的对象
 * @param var2 var2字段内存地址偏移量
 * @param var4 需要加的值
 * @return 返回旧值
 */
public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
    return var6;
}

/**
 * int类型值原子操作方法,将var2地址对应的值置为var4
 *
 * @param var1 操作的对象
 * @param var2 var2字段内存地址偏移量
 * @param var4 新值
 * @return 返回旧值
 */
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var4));
    return var5;
}

/**
 * long类型值原子操作方法,将var2地址对应的值置为var4
 *
 * @param var1 操作的对象
 * @param var2 var2字段内存地址偏移量
 * @param var4 新值
 * @return 返回旧值
 */
public final long getAndSetLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var4));
    return var6;
}

/**
 * Object类型值原子操作方法,将var2地址对应的值置为var4
 *
 * @param var1 操作的对象
 * @param var2 var2字段内存地址偏移量
 * @param var4 新值
 * @return 返回旧值
 */
public final Object getAndSetObject(Object var1, long var2, Object var4) {
    Object var5;
    do {
        var5 = this.getObjectVolatile(var1, var2);
    } while (!this.compareAndSwapObject(var1, var2, var5, var4));
    return var5;
}

4.Unsafe中线程调度的相关方法

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程,isAbsolute:是否是绝对时间,如果为true,time是一个绝对时间,如果为false,time是一个相对时间,time表示纳秒
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

调用park后,线程将被阻塞,直到unpark调用或者超时,如果之前调用过unpark,不会进行阻塞,即parkunpark不区分先后顺序。monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了。

5.例子

package com.example.juc.CAS;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author diao 2022/4/19
 */
public class CountTheNumberRequest {
    static Unsafe unsafe;
    /*1.记录网站访问量,每次访问+1*/
    static int count;
    /*2.count的地址偏移量*/
    static long countOffset;
    
    //2.得到unsafe,并且计算出变量count的偏移地址
    static{
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            /*3.获取theUnsafe变量的值*/
            unsafe = (Unsafe) field.get(null);
            /**
             * 计算变量count的偏移地址
             * 1.首先通过反射得到count的值
             * 2.然后通过unsafe调用staticFieldOffset得到该变量的偏移地址
             */
            Field countField = CountTheNumberRequest.class.getDeclaredField("count");
            countOffset = unsafe.staticFieldOffset(countField);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    //3.模拟访问网站
    public static void request() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(10);
        unsafe.getAndAddInt(CountTheNumberRequest.class,countOffset,1);
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        int ThreadSize=100;
        List<Thread> list=new ArrayList<>(); 
        
        /*100个线程进行访问*/
        for (int i = 0; i < ThreadSize; i++) {
           list.add(new Thread(()->{
               /*每个线程访问10次网站*/
               for (int j = 0; j < 10; j++) {
                   try {
                       request();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }));
        }
        
        //4.1start
        list.forEach(Thread::start);
        
        //4.2等待所有线程访问完毕
        list.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+".耗时:"+(end-startTime)+",count="+count);
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fairy要carry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值