Java线程池学习总结

一、前言

最近在复习Java线程池,做个笔记~
在这里插入图片描述

二、线程池介绍

线程池是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后创建线程自动启动并执行任务。优势在于:

(1)线程和任务分离,可对所有线程进行统一的管理和控制,提升线程重用性;
(2)控制线程并发数量,降低服务器压力,统一管理所有线程;
(3)提升系统响应速度,假设创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那使用线程池就减少T1和T3的时间;

应用场景:

1. 网购商品秒杀
2. 云盘文件上传和下载
3. 12306网上购票系统等

有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;

三、线程池处理任务的流程

对于Java内置线程池而言,流程通俗描述为:

1)若当前运行的线程数小于核心线程数,就会新建一个线程来执行任务。
(2)若当前运行的线程数等于或大于核心线程数,但小于最大线程数,就把该任务放入到任务队列里等待执行。
(3)若向任务队列投放任务失败(任务队列已满),但当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
(4)若当前运行的线程数已经等同于最大线程数,新建线程将会使当前运行的线程超出最大线程数,那当前任务会被拒绝,饱和策略会调RejectedExecutionHandler.rejectedExecution()方法。

如图:
在这里插入图片描述
即是说,新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

四、线程池种类

4.1 ThreadPoolExecutor

4.1.1 ThreadPoolExecutor的七个参数

部分构造器源码:

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                              int maximumPoolSize,//     最大线程数
                              long keepAliveTime, //       最大空闲时间
                              TimeUnit unit,         //        时间单位
                              BlockingQueue<Runnable> workQueue,   //   任务队列
                              ThreadFactory threadFactory,    // 线程工厂
                              RejectedExecutionHandler handler  //  饱和处理机制(拒绝策略)
	) 
{ ... }

任务队列(阻塞队列):

1LinkedBlockingQueue(无界队列):容量为 Integer.MAX_VALUE。可分为---FixedThreadPool:最多只能创建核心线程数的线程 和 SingleThreadExector:只能创建一个线程
(2SynchronousQueue(同步队列):CachedThreadPool:最大线程数是 Integer.MAX_VALUE---保证对提交的任务,若有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。
(3DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPoolSingleThreadScheduledExecutor。内部元素不是按照放入的时间排序,而是按照延迟的时间长短对任务进行排序,内部采用的堆数据结构,保证每次出队的任务都是当前队列中执行时间最靠前的。最多只能创建核心线程数的线程。

饱和处理机制(拒绝策略):

1AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
(2CallerRunsPolicy:直接在调用execute()方法的线程中运行被拒绝的任务,若执行程序已关闭,则会丢弃该任务。
(3DiscardPolicy:不处理新任务,直接丢弃掉。
(4DiscardOldestPolicy:丢弃最早未处理的任务请求。

4.1.2 ThreadPoolExecutor使用

创建一个固定大小为5的线程池(有5个线程),任务队列使用LinkedBlockingQueue,最大容量为100000,拒绝策略设置为CallerRunsPolicy,表示若任务过多,提交任务的线程将会自己运行这个任务。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class ThreadPoolExecutorExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        int nThreads = 5; // 线程池中的线程数
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            nThreads, // 核心线程数
            nThreads, // 最大线程数
            0L, TimeUnit.MILLISECONDS, // 保持活跃时间
            new LinkedBlockingQueue<Runnable>(100000), // 任务队列
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
        
        // 使用线程池...
        
        // 关闭线程池
        executor.shutdown();
    }
}

4.2 Java内置线程池ExecutorService

4.2.1 ExecutorService介绍

ExecutorService接口是java内置的线程池接口,java内置线程池的基本使用
常用方法:

 void shutdown():启动一次顺序关闭,执行以前提交的任务,但不接受新任务。 
 List<Runnable> shutdownNow():停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。 
<T> Future<T> submit(Callable<T> task):执行带返回值的任务,返回一个Future对象。 
 Future<?> submit(Runnable task):执行 Runnable 任务,并返回一个表示该任务的 Future<T> Future<T> submit(Runnable task, T result):执行 Runnable 任务,并返回一个表示该任务的 Future

使用JDK中的Executors 类中的静态方法获取ExecutorService的线程池:

static ExecutorService newCachedThreadPool() :
		  创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建 
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) :
          线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行; 
static ExecutorService newFixedThreadPool(int nThreads)   :
		  创建一个可重用固定线程数的线程池 
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) :
          创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建。 
static ExecutorService newSingleThreadExecutor() :
          创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。 
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) :
          创建一个使用单个 worker 线程的 Executor,且线程池中的所有线程都使用ThreadFactory来创建。 

4.2.2 newCachedThreadPool使用

package com.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/*
    练习Executors获取ExecutorService,然后调用方法,提交任务;
 */
public class MyTest01 {
    public static void main(String[] args) {
//        test1();
        test2();
    }
    //练习newCachedThreadPool方法
    private static void test1() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool();
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable(i));
        }
    }
    private static void test2() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool(new ThreadFactory() {
            int n=1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义的线程名称"+n++);
            }
        });
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable(i));
        }
    }
}
/*
    任务类,包含一个任务编号,在任务中,打印出是哪一个线程正在执行任务
 */
class MyRunnable implements Runnable{
    private  int id;
    public MyRunnable(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        //获取线程的名称,打印一句话
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务..."+id);
    }
}

在这里插入图片描述

4.2.3 newFixedThreadPool使用

package com.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/*
    练习Executors获取ExecutorService,然后调用方法,提交任务;
 */
public class MyTest02 {
    public static void main(String[] args) {
        //test1();
        test2();
    }
    //练习方法newFixedThreadPool
    private static void test1() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newFixedThreadPool(3);
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable2(i));
        }
    }
    private static void test2() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newFixedThreadPool(3,new ThreadFactory() {
            int n=1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义的线程名称"+n++);
            }
        });
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable2(i));
        }
    }
}

/*
    任务类,包含一个任务编号,在任务中,打印出是哪一个线程正在执行任务
 */
class MyRunnable2 implements Runnable{
    private  int id;
    public MyRunnable2(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        //获取线程的名称,打印一句话
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务..."+id);
    }
}

在这里插入图片描述

4.2.4 newSingleThreadExecutor使用

package com.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/*
    练习Executors获取ExecutorService,然后调用方法,提交任务;
 */
public class MyTest03 {
    public static void main(String[] args) {
        //test1();
        test2();
    }
    //练习方法newFixedThreadPool
    private static void test1() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newSingleThreadExecutor();
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable3(i));
        }
    }
    private static void test2() {
        //1:使用工厂类获取线程池对象
        ExecutorService es = Executors.newSingleThreadExecutor(new ThreadFactory() {
            int n=1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义的线程名称"+n++);
            }
        });
        //2:提交任务;
        for (int i = 1; i <=10 ; i++) {
            es.submit(new MyRunnable3(i));
        }
    }
}

/*
    任务类,包含一个任务编号,在任务中,打印出是哪一个线程正在执行任务
 */
class MyRunnable3 implements Runnable{
    private  int id;
    public MyRunnable3(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        //获取线程的名称,打印一句话
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务..."+id);
    }
}

在这里插入图片描述

4.3 Java内置线程池ScheduledExecutorService

4.3.1 ScheduledExecutorService介绍

ScheduledExecutorService是ExecutorService的子接口,具备延迟运行或定期执行任务的能力。
常用获取方式如下:

static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
          创建一个可重用固定线程数的线程池且允许延迟运行或定期执行任务;
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
          创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建,且允许延迟运行或定期执行任务; 
static ScheduledExecutorService newSingleThreadScheduledExecutor() 
          创建一个单线程执行程序,它允许在给定延迟后运行命令或者定期地执行。 
static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) 
          创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 

ScheduledExecutorService常用方法如下:

<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) 
          延迟时间单位是unit,数量是delay的时间后执行callable。 
 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 
          延迟时间单位是unit,数量是delay的时间后执行command。  
 ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 
         延迟时间单位是unit,数量是initialDelay的时间后,每间隔period时间重复执行一次command。 
 ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 
          创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。 

4.3.2 newScheduledThreadPool使用

package com.test.demo3;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/*
    测试ScheduleExecutorService接口中延迟执行任务和重复执行任务的功能
 */
public class ScheduleExecutorServiceDemo01 {
    public static void main(String[] args) {
        //1:获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newScheduledThreadPool(3);
        //2:创建多个任务对象,提交任务,每个任务延迟2秒执行
        for (int i=1;i<=10;i++){
            es.schedule(new MyRunnable(i),2, TimeUnit.SECONDS);
        }
        System.out.println("over");
    }
}
class MyRunnable implements Runnable{
    private int id;

    public MyRunnable(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务:"+id);

    }
}

在这里插入图片描述

4.3.3 scheduleAtFixedRate()方法

package com.test.demo3;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/*
    测试ScheduleExecutorService接口中延迟执行任务和重复执行任务的功能
 */
public class ScheduleExecutorServiceDemo02 {
    public static void main(String[] args) {
        //1:获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newScheduledThreadPool(3, new ThreadFactory() {
            int n = 1;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"自定义线程名:"+n++);
            }
        });
        //2:创建多个任务对象,提交任务,每个任务延迟2秒执行
         es.scheduleAtFixedRate(new MyRunnable2(1),1,2,TimeUnit.SECONDS);
        System.out.println("over");
    }
}

class MyRunnable2 implements Runnable{
    private int id;

    public MyRunnable2(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name+"执行了任务:"+id);
    }
}

4.4 Java内置线程池Future(异步计算结果)

4.4.1 Future介绍

上述Java内置线程池使用时,没有考虑线程计算的结果,但开发中有时需要利用线程进行一些计算,然后获取这些计算的结果,可通过Future 对象获取线程异步计算的结果。Future 的常用方法如下:

boolean cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。 
 V get() 如有必要,等待计算完成,然后获取其结果。 
 V get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 
 boolean isCancelled() 如果在任务正常完成前将其取消,则返回 trueboolean isDone() 如果任务已完成,则返回 true

4.4.2 Future使用

package com.itheima.demo04;

import java.util.concurrent.*;

/*
    练习异步计算结果
 */
public class FutureDemo {
    public static void main(String[] args) throws Exception {
        //1:获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool();
        //2:创建Callable类型的任务对象
        Future<Integer> f = es.submit(new MyCall(1, 1));
        //3:判断任务是否已经完成
        //test1(f);
        boolean b = f.cancel(true);
        //System.out.println("取消任务执行的结果:"+b);
        //Integer v = f.get(1, TimeUnit.SECONDS);//由于等待时间过短,任务来不及执行完成,会报异常
        //System.out.println("任务执行的结果是:"+v);
    }
    //正常测试流程
    private static void test1(Future<Integer> f) throws InterruptedException, ExecutionException {
        boolean done = f.isDone();
        System.out.println("第一次判断任务是否完成:"+done);
        boolean cancelled = f.isCancelled();
        System.out.println("第一次判断任务是否取消:"+cancelled);
        Integer v = f.get();//一直等待任务的执行,直到完成为止
        System.out.println("任务执行的结果是:"+v);
        boolean done2 = f.isDone();
        System.out.println("第二次判断任务是否完成:"+done2);
        boolean cancelled2 = f.isCancelled();
        System.out.println("第二次判断任务是否取消:"+cancelled2);
    }
}
class MyCall implements Callable<Integer>{
    private int a;
    private int b;
    //通过构造方法传递两个参数

    public MyCall(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        String name = Thread.currentThread().getName();
        System.out.println(name+"准备开始计算...");
        Thread.sleep(2000);
        System.out.println(name+"计算完成...");
        return a+b;
    }
}

五、实战案例

5.1 秒杀商品

假设商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假设有20人同时参与该活动,使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败。
要求:
1:使用线程池创建线程
2:解决线程安全问题
思路提示:
1:既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
2:当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
3:使用synchronized控制线程安全,防止出现错误数据;
代码步骤:
1:编写任务类,主要是送出手机给秒杀成功的客户;
2:编写主程序类,创建20个任务(模拟20个客户);
3:创建线程池对象并接收20个任务,开始执行任务;

1.创建任务类: 包含商品数量,客户名称,送手机的行为;

package com.itheima.demo05;
/*
    任务类:包含商品数量,客户名称,送手机的行为;
 */
public class MyTask implements Runnable {
    //设计一个变量,用于表示商品的数量
    private static int id = 10;
    //表示客户名称的变量
    private String userName;

    public MyTask(String userName) {
        this.userName = userName;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在使用"+name+"参与秒杀任务...");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(id>0){
                System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!");
            }else {
                System.out.println(userName+"使用"+name+"秒杀失败啦!");
            }
        }
    }
}

2.主程序类,测试任务类

package com.itheima.demo05;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/*
    主程序类,测试任务类
 */
public class MyTest {
    public static void main(String[] args) {
        //1:创建一个线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new LinkedBlockingQueue<>(15));
        //2:循环创建任务对象
        for (int i = 1; i <=20 ; i++) {
            MyTask myTask = new MyTask("客户"+i);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

5.2 取钱

设计一个程序,使用两个线程模拟在两个地点同时从一个账号中取钱,假如卡中一共有1000元,每个线程取800元,要求演示结果一个线程取款成功,剩余200元,另一个线程取款失败,余额不足;
要求:
1:使用线程池创建线程
2:解决线程安全问题
思路:
1:线程池可以利用Executors工厂类的静态方法,创建线程池对象;
2:解决线程安全问题可以使用synchronized方法控制取钱的操作
3:在取款前,先判断余额是否足够,且保证余额判断和取钱行为的原子性;

package com.itheima.demo06;

public class MyTask implements Runnable {
    //用户姓名
    private String userName;
    //取款金额
    private double money;
    //总金额
    private static double total = 1000;

    public MyTask(String userName, double money) {
        this.userName = userName;
        this.money = money;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(total-money>0){
                System.out.println(userName+"使用"+name+"取款:"+money+"元成功,余额:"+(total-money));
                total-=money;
            }else {
                System.out.println(userName+"使用"+name+"取款:"+money+"元失败,余额:"+total);
            }
        }
    }
}
package com.itheima.demo06;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class MyTest {
    public static void main(String[] args) {
        //1:创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            int id = 1;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "ATM" + id++);
            }
        });
        //2:创建两个任务并提交
        for (int i = 1; i <=2 ; i++) {
            MyTask myTask = new MyTask("客户" + i, 800);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

六、使用总结

线程池的使用步骤:

1:利用Executors工厂类的静态方法,创建线程池对象;
2:编写RunnableCallable实现类的实例对象;
3:利用ExecutorService的submit方法或ScheduledExecutorService的schedule方	法提交并执行线程任务
4:如果有执行结果,则处理异步执行结果(Future)
5:调用shutdown()方法,关闭线程池

七、面试题

7.1 如果提交任务时,线程池队列已满,会发生什么?

1、若使用无界队列 LinkedBlockingQueue,线程池队列已满,会继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可认为是一个无穷大的队列,可以无限存放任务。
2、若使用有界队列(比如ArrayBlockingQueue),任务首先会被添加到 ArrayBlockingQueue 中,线程池队列已满,会根据 maximumPoolSize 的值增加线程数量,若增加线程数量依然处理不过来, ArrayBlockingQueue继续满,那就会使用拒绝策略 RejectedExecutionHandler 处理满的任务,默认是AbortPolicy,抛出异常。

7.2 线程的调度策略

线程调度器选择优先级最高的线程运行,但若发生以下情况,就会终止线程的运行:
1、线程体中调用yield方法让出对 cpu 的占用权利
2、线程体中调用sleep方法使线程进入睡眠状态
3、线程由于IO操作受到阻塞
4、另外一个更高优先级线程出现
5)在支持时间片的系统中,该线程的时间片用完

7.3 线程池的七个参数

见上文

7.4 线程池的拒绝策略

见上文

7.5 什么是线程池?有哪几种创建方式?

见上文

7.6 常见线程池有哪些?

见上文

7.7 线程池中线程异常后,销毁还是复用?

线程池中的线程在发生异常后,不会立即被销毁,而是继续复用。
线程池通过Worker类和runWorker方法实现了这一机制,即使任务抛出异常,线程仍然会继续尝试执行新的任务,直到满足特定条件时才会退出。
这种设计的优点在于提高线程的利用率,减少频繁创建和销毁线程的开销。在使用线程池时,需注意任务中的异常处理,要确保异常不会影响整个应用程序的稳定性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

容若只如初见

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

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

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

打赏作者

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

抵扣说明:

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

余额充值