集合类不安全之并发修改异常以及线程池

集合类不安全之并发修改异常以及线程池

1、ArrayList线程不安全的例子?

1.故障现象:
-----|java.util.ConcurrentModificationExcption >>高并发异常

2.导致原因:
并发争抢修改导致,参考我们的花名册签名情况。
一个人正在写入,另一同学过来抢夺,导致数据不一致异常。并发修改异常

3.解决办法:
3.1 new Vector<>();
3.2 Collections.synchronizedList(new ArrayList<>());
3.3 new CopyOnWriteArrayList<>();
写时复制:copyOnWrite容器即写时复制的容器。向一个容器添加元素时,不直接向当前Object[]添加,而是将当前容器进行copy,得到Object[] newElements,然后向新的容器Object[] newElements里添加元素,再将原容器的引用指向新的容器setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发的读。而不需加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
4.优化建议:

2、set线程不安全的例子?

​ 解决方法:
​ Collections.synchronizedSet(new HashSet<>());
​ new CopyOnWriteSet<>(); — 底层源码还是一个CopyOnWriteArrayList

​ HashSet — 底层是HashMap,那为什add(key),参数只有一个?
​ 源码:这个参数是put(key,value)的key,value人家已经定义好了。

3、Map线程不安全的例子?

​ 解决方法:
​ Collections.synchronizedMap(new HashMap<>());
​ new ConcurrentHashMap<>(); — 底层源码还是一个?

4、公平锁/非公平锁/可重入锁/自旋锁,谈谈理解,手写一个自旋锁?

并发包中ReentrantLock的创建可以指定构造函数的Boolean类型来得到公平锁或非公平锁,默认是非公平锁。

公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的同学比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或饥饿现象。


可重入锁(递归锁):最大作用----避免死锁
	同一线程外层函数获得锁之后,内层递归函数任然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

	也就是,线程可以进入任何一个它已经拥有的锁所同步者的代码块。

	ReentrantLock/Synchronized就是一个典型的可重入锁

自旋锁(spinlock):指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
import java.sql.Time;
			import java.util.concurrent.TimeUnit;
			import java.util.concurrent.atomic.AtomicReference;

			public class TestMyLock {
				//原子引用线程
				AtomicReference<Thread> atomicReference = new AtomicReference<>();
				Thread thread = new Thread();
				public void myLock(){
					System.out.println(Thread.currentThread().getName()+"\t come in ……");
					while(!atomicReference.compareAndSet(null,thread)){}
				}

				public void myUnLock(){
					atomicReference.compareAndSet(thread,null);
					System.out.println(Thread.currentThread().getName()+"\t invoked myUnLock");
				}

				public static void main(String[] args) {

					TestMyLock testMyLock = new TestMyLock();

					new Thread(() -> {
						testMyLock.myLock();
						//睡眠一会
						try{
							TimeUnit.SECONDS.sleep(5);
						} catch (InterruptedException e){
							e.printStackTrace();
						}
						testMyLock.myUnLock();
					},"AA").start();

					try {
						TimeUnit.SECONDS.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

					new Thread(() -> {
						testMyLock.myLock();
						try {
							TimeUnit.SECONDS.sleep(1);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						testMyLock.myUnLock();
					},"BB").start();

				}

			}

独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有。

对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

//资源类
class MyCache{
	private volatile Map<String,Object> map = new HashMap<>();
	//private Lock lock = new ReentrantLock();//每次只能有一个线程同时进行读写操作
	private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

	//写
	public void put(String key,Object value){
		reentrantReadWriteLock.writeLock().lock();//加锁
		try{
			System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key);
			//线程睡眠一会
			try {
				TimeUnit.MICROSECONDS.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			map.put(key, value);
			System.out.println(Thread.currentThread().getName()+"\t 写入完成:"+key);
		}catch (Exception e){
			e.printStackTrace();
		}finally {
			reentrantReadWriteLock.writeLock().unlock();//释放锁
		}
	}

	//读
	public void get(String key){
		reentrantReadWriteLock.readLock().lock();
		try {
			System.out.println(Thread.currentThread().getName()+"\t 正在读取:"+key);
			//线程睡眠一会
			try {
				TimeUnit.MICROSECONDS.sleep(300);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			Object result = map.get(key);
			System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+key);
		}catch (Exception e){
			e.printStackTrace();
		}finally {
			reentrantReadWriteLock.readLock().unlock();
		}
	}
}

/**
 * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源该可以同时进行
 * 但是
 * 如果一个线程想去写共享资源来,就不应该再有其他线程可以对该资源进行读写
 * 小总结:
 *          读- 读能共存
 *          读- 写不能共存
 *          写- 写不能共存
 *
 *          写操作:原子+独占
 */
public class ReadWriteLockDemo {

	public static void main(String[] args) {

		MyCache myCache = new MyCache();

		//5个线程写操作
		for (int i = 1; i <= 5 ; i++) {
			final int tempInt = i;
			new Thread(()->{
				myCache.put(tempInt+"",tempInt);
			},String.valueOf(i)).start();
		}

		//5个线程读操作
		for (int i = 1; i <= 5 ; i++) {
			final int tempInt = i;
			new Thread(()->{
				myCache.get(tempInt+"");
			},String.valueOf(i)).start();
		}


	}
}

5、CountDownLatch/CyclicBarrier/Semaphore使用过吗?

​ CountDownLatch:
​ 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
​ CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。
​ 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因调用await方法被阻塞的线程会被唤醒,继续执行。

public enum CountryEnum {
	ONE(1,"齐"),TWO(2,"楚"),THREE(3,"燕"),FOUR(4,"赵"),FIVE(5,"魏"),SIX(6,"韩");

	private Integer retCode;
	private String retMessage;

	public Integer getRetCode() {
		return retCode;
	}

	public String getRetMessage() {
		return retMessage;
	}

	CountryEnum(Integer retCode, String retMessage) {
		this.retCode = retCode;
		this.retMessage = retMessage;
	}

	public static CountryEnum forEach_CountryEnum(int index){
		CountryEnum[] myArray = CountryEnum.values();
		for(CountryEnum element : myArray){
			if(index == element.getRetCode())
				return element;
		}
		return null;
	}
}

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(6);//从6倒计时
		for (int i = 1; i <= 6; i++) {
			new Thread(()->{
				System.out.println(Thread.currentThread().getName()+"\t国,被灭……");
				countDownLatch.countDown();
			},CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
		}
		countDownLatch.await();
		System.out.println(Thread.currentThread().getName()+">>>>>>>>>大秦一统天下!");
	}

}

CyclicBarrier:字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的就是让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> {System.out.println(">>>>>召唤神龙");});

		for(int i=1;i<=7;i++){
			final int tempInt = i;
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName()+"\t收集到第:"+tempInt+"颗龙珠");
				try {
					cyclicBarrier.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					e.printStackTrace();
				}
			},String.valueOf(i)).start();
		}
	}
}
Semaphore:信号量
		两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

6、阻塞队列:

​ 用在哪?
​ 生产者消费者模式
​ 线程池
​ 消息中间件

SynchronousQueue没有容量。
	与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue.

	每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

7、Synchronized和Lock有什么区别?用新的Lock有什么好处?举例说说

​ 1.原始构成:
​ synchronized:关键字属于JVM层面;
​ monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调用wait/notify等方法)
​ monitorexit–释放锁
​ Lock:具体类(java.util.concurrent.locks.lock)是api层面的锁
​ 2.使用方法:
​ synchronized:不需要用户手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用;
​ ReentrantLock:需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现象。
​ 需要lock()和unlock()方法配合try/finally语句块来完成。
​ 3.等待是否可中断:
​ synchronized:不可中断,除非抛出异常或者正常运行完成
​ ReentrantLock:可中断
​ 1)设置超时方法tryLock(long timeout,TimeUnit unit);
​ 2)lockInterruptibly()放代码块中,调用interrupt()方法可中断。
​ 4.加锁是否公平
​ synchronized:非公平锁
​ ReentrantLock:两者都可以,默认是非公平锁,构造方法可以传入Boolean值,true为公平锁,false为非公平锁
​ 5.锁绑定多个条件Condition
​ synchronized:没有
​ ReentrantLock:用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么全部唤醒。

题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
	AA打印5次,BB打印10次,CC打印15次
	紧接着
	AA打印5次,BB打印10次,CC打印15次
	…………
	走10轮
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
 * 		AA打印5次,BB打印10次,CC打印15次
 * 		紧接着
 * 		AA打印5次,BB打印10次,CC打印15次
 * 		…………
 * 		走10轮
 */
class ShareResource{
    private int number = 1;//A:1    B:2  C:3 标志位
    private Lock lock = new ReentrantLock();

    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //线程A
    public void print5(){
        lock.lock();
        try {
            //1 判断
            while (number != 1) {
                c1.await();
            }
            //2 干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //3 通知--下一个(指定)线程干活
            number = 2;
            c2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    //线程B
    public void print10(){
        lock.lock();
        try {
            //1 判断
            while (number != 2) {
                c2.await();
            }
            //2 干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //3 通知--下一个(指定)线程干活
            number = 3;
            c3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    //线程C
    public void print15(){
        lock.lock();
        try {
            //1 判断
            while (number != 3) {
                c3.await();
            }
            //2 干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            //3 通知--下一个(指定)线程干活
            number = 1;
            c1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                shareResource.print5();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                shareResource.print10();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                shareResource.print15();
            }
        },"C").start();
    }
}

在多线程领域,所谓阻塞,在某些情况下会挂起线程(阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都帮你解决了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其要兼顾效率和线程安全,给我们带来了不小的复杂度。
package com.itguigu;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

class MyRescource{
	private volatile boolean FLAG = true;//默认开启,进行生产+消费
	private AtomicInteger atomicInter = new AtomicInteger();
	BlockingQueue<String> blockingQueue = null;

	public MyRescource(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
		System.out.println(blockingQueue.getClass().getName());
	}
	public void myProd() throws Exception{
		String data = null;
		boolean retValue;
		while(FLAG){
			data = atomicInter.incrementAndGet()+"";
			retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
			if(retValue){
				System.out.println(Thread.currentThread().getName()+"\t 生产:蛋糕"+data+"成功");
			}else{
				System.out.println(Thread.currentThread().getName()+"\t 生产:蛋糕"+data+"失败");
			}
			TimeUnit.SECONDS.sleep(1);
		}
		System.out.println(Thread.currentThread().getName()+"\t大老板叫停了,表示FLAG=false,生产动作结束");
	}

	public void myConsumer() throws Exception{
		String result = null;
		while(FLAG){
			result = blockingQueue.poll(2L,TimeUnit.SECONDS);
			if(null == result || "".equalsIgnoreCase(result)){
				FLAG = false;
				System.out.println(Thread.currentThread().getName()+"\t 超过连两秒没有取到蛋糕,退出");
				System.out.println();
				System.out.println();
				return;
			}
			System.out.println(Thread.currentThread().getName()+"消费蛋糕:"+result+"成功");
		}
	}

	public void stop() {
		this.FLAG = false;
	}
}

/**
 * volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
 */
public class ProdConsumer_BlockQueueDemo {
	public static void main(String[] args) {
		MyRescource myRescource = new MyRescource(new ArrayBlockingQueue<>(10));

		new Thread(()->{
			System.out.println(Thread.currentThread().getName()+"\t生产线程启动");
			try {
				myRescource.myConsumer();
			} catch (Exception e) {
				e.printStackTrace();
			}
		},"Prod").start();

		new Thread(()->{
			System.out.println(Thread.currentThread().getName()+"\t消费线程启动");
			try {
				myRescource.myConsumer();
			} catch (Exception e) {
				e.printStackTrace();
			}
		},"Consumer").start();

		//线程暂停一会
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		System.out.println();
		System.out.println();
		System.out.println();

		System.out.println("5秒钟时间到,大老板main线程叫停,活动结束");
		myRescource.stop();
	}

}

8、线程池

​ 为什么用线程池,优势?
​ 线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务,如果线程数量超过最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

	特点:线程复用;控制最大并发数;管理线程。

	1)降低资源消耗。通过重复利用自己创建的线程来降低线程创建和销毁造成的消耗。
	2)提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
	3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行同一分配,调优和监控。

线程池如何使用?



线程池的几个重要参数介绍?(7大参数)
	corePoolSize:线程池中的常驻核心线程数
	maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
	keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
	unit:keepAliveTime的单位
	workQueue:任务队列,被提交但尚未被执行的任务
	threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认即可。
	handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝

说说线程池的底层工作原理?
package com.itguigu;

import java.util.concurrent.*;

/**
 * 线程池
 * Array Arrays
 * Collection Collections
 * Executor Executors   ---Executors工具类
 * 第4中获得/使用java多线程的方式,线程池
 */
public class MyThreadPoolDemo {
    public static void main(String[] args) {
        //自定义线程池,工作中使用
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        //最大线程数=阻塞队列数+最大线程数
        /*
        ThreadPoolExecutor:拒绝策略(4种)
            ThreadPoolExecutor.AbortPolicy()----粗暴,超过最大线程数,上来就报异常,默认
            ThreadPoolExecutor.CallerRunsPolicy()---不会丢弃,也不会抛异常,而是回退给上级线程(如:main)
            ThreadPoolExecutor.DiscardOldestPolicy()---丢弃等待最久的任务,然后把当前任务加入队列中尝试再次提交任务
            ThreadPoolExecutor.DiscardPolicy()---直接丢弃任务

         */
        try {
            for (int i=1;i<=9;i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }

    //调用线程
    public static void threadPoolInit(){
        //使用池化,不建议使用new
        ExecutorService threadPool = Executors.newFixedThreadPool(5);//固定数线程池
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();//1池1线程
        //ExecutorService threadPool = Executors.newCachedThreadPool();//1池N线程

        try {
            //模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
            for (int i=1;i<=10;i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

9、如何配置策略,你是如何考虑的?

两种:CPU密集型、IO密集型
CPUT密集型:该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)

CPU密集型的任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池

IO密集:
1-> IO密集型的任务线程并不是一直在执行任务,则配置尽可能的多的线程
2-> 即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运行能力浪费在等待。所以IO密集型任务中使用多线程可以大大加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:
	参考公式:CPU核数/(1-阻塞系数)	阻塞系数在0.8~0.9之间

​ 比如8核CPU:8/(1-0.9) = 80个线程数

死锁编码及定位分析?
死锁产生原因:系统资源不足、进程运行推进的顺序不合理、资源分配不当

package com.itguigu;

import java.util.concurrent.TimeUnit;

class HoldLockThread implements Runnable{

	private String lockA;
	private String lockB;

	public HoldLockThread(String lockA, String lockB) {
		this.lockA = lockA;
		this.lockB = lockB;
	}

	@Override
	public void run() {
		synchronized (lockA){
			System.out.println(Thread.currentThread().getName()+"\t持有:"+lockA+"去尝试获取:"+lockB);
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (lockB){
				System.out.println(Thread.currentThread().getName()+"\t持有:"+lockB+"去尝试获取:"+lockA);
			}
		}
	}
}
/**
 * 死锁
 * 解决:
 *  jps命令定位进程号
 *  jstack找到死锁查看
 */
public class DeadLockDemo {
	public static void main(String[] args) {
		String lockA = "lockA";
		String lockB = "lockB";

		new Thread(new HoldLockThread(lockA,lockB),"ThreadAAA").start();
		new Thread(new HoldLockThread(lockB,lockA),"ThreadBBB").start();

		 /*
		 linux ps -ef|grep xxx  ls -l
		 window下的java云讯程序  也有类似ps的查看进程的命令,但是目前我们需要查看的是
			jps = java ps       jps -l
		  */
	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值