6-多线程

本文详细介绍了Java多线程的基础知识,包括并行与并发的区别、进程与线程的概念,以及Java线程调度方式。接着讲解了线程的创建,包括继承Thread类、实现Runnable接口以及匿名内部类三种方式,并对比了实现Runnable的优势。此外,文章深入探讨了线程安全问题,如可见性、有序性和原子性,以及如何通过同步机制(Synchronized、Lock)、Volatile关键字和Atomic类来解决。最后,讨论了并发包中的线程安全容器,如CopyOnWriteArrayList、ConcurrentHashMap以及并发工具类CountDownLatch、CyclicBarrier、Semaphore和Exchanger。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录


前言

提示:这里介绍了多线程的基础知识点,及注意事项


提示:以下是本篇文章正文内容,下面案例可供参考

一、多线程的概述

1、并行与并发:

并行:指两个或多个事件在同一时刻发生(同时执行);
并发:之两个或多个事件在同一时间段内发生(交替执行)
并行与并发

2、进程与线程

进程:进程是程序的一次执行过程,是系统运行程序的基本单位;

1、进程是应用程序的可执行单元
2、一个应用程序可以有多个进程
3、每个进程执行都会有独立的内存空间

线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一条线程。

线程是进程的可执行单元
一个进程可以有多条线程
每个线程执行都会有独立的内存空间

3、多线程的注意点:

1、java只有单进程,然后有多线程。
2、一个进程只能执行一条线程,所以java中只有多线程并发,没有多线程并行。

4、线程的调度:

4.1、分时调度:

所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的使用时间。

4.2、抢占式调度:

优先让优先级高的线程使用cpu,如果优先级相同,那么会随机选一个(线程随机性)


注意了注意了!!!

java线程的调度方式是抢占式。


5、线程的创建方式:

5.1、继承的方式(继承Thread类)

案例:
public class MyThread extends Thread {

    @Override
    public void run() {
        // 线程需要执行的任务代码
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程i的值是:"+i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            - 创建一个子类继承Thread类
            - 在子类中重写run方法,把线程需要执行的任务代码放入run方法中
            - 创建子类对象,调用start()方法启动线程,执行任务

            注意:
                1.线程不能重复启动,只能启动一次
                2.启动线程,一定要调用start()方法
         */
        // 创建线程对象
        MyThread mt = new MyThread();
        // 启动线程,执行任务
        mt.start();// 默认调用run方法


        // 线程需要执行的任务代码
        for (int j = 0; j < 100; j++) {
            System.out.println("主线程j的值是:"+j);
        }
    }
}

5.2、实现的方式(实现Runnable接口)

案例:
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程需要执行的任务代码
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程i的值是:"+i);
        }
    }
}


public class Test {
    public static void main(String[] args) {
        /*
            实现步骤:
                - 创建实现类实现Runnable接口
                - 在实现类中,重写run方法,把线程需要执行的任务代码放入run方法中
                - 创建实现类对象
                - 创建Thread线程对象,并传入实现类对象
                - 使用Thread线程对象调用start方法启动线程,执行任务
         */
        // 创建实现类对象
        MyRunnable mr = new MyRunnable();
        // 创建Thread线程对象
        Thread t = new Thread(mr);
        // 启动线程执行任务
        t.start();

        // 主线程需要执行的任务代码
        for (int j = 0; j < 100; j++) {
            System.out.println("主线程j的值是:"+j);
        }
    }
}

5.3、匿名内部类的实现方式:

案例:
public class Test {
    public static void main(String[] args) {
        /*
            - 创建Thread线程对象,并传入Runnable接口的匿名内部类
            - 在Runnable匿名内部类中重写run方法,书写线程需要执行的任务代码
            - 使用Thread线程对象调用start方法启动线程,执行任务
         */
        // 创建线程对象,传入任务对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                // 线程任务需要执行的任务代码
                for (int i = 0; i < 100; i++) {
                    System.out.println("子线程i的值是:"+i);
                }
            }
        });
        // 启动线程,执行任务
        t.start();

        // 主线程需要执行的任务代码
        for (int j = 0; j < 100; j++) {
            System.out.println("主线程j的值是:"+j);
        }
    }
}

6、实现方式创建线程的优势:

**

实现Runnable比继承Thread类所具有的优势:

**

1、适合多个相同的程序代码的线程去共享同一个资源(任务)
2、可以避免java中单继承的局限性
3、增加程序的健壮性,实现解耦操作;代码可以被多个线程共享,实现代码和线程独立
4、线程池中只能放入实现Runnable和Callable类线程,不能直接放入继承Thread的类

二、多线程中的线程安全问题

1、高并发及线程安全的原因:

1、高并发:是指在某个时间点上,有许多用户(线程)同时访问统一资源。例如双十一、12306在线抢票,都会面临大量用户同时抢购同一件商品/票的情况。
2、线程安全:当我们使用多个线程访问同一个资源的时候,并且多个线程对该资源有写的操作,可能会导致被访问的资源出现“数据污染”,就容易出现线程安全问题。

2、多线程的运行机制:

2.1、原理:抢占式调度

2.2、特点:

1、当一个线程启动后,JVM会为其分配一个独立的“线程栈区”,这个线程会在这个独立的栈区运行。
2、结论:线程启动了就会在栈内存中开辟一块独立的栈空间,来执行该线程的任务代码。

public class MyThread extends Thread {

  @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程i的值是:" + i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建并启动子线程
        MyThread mt = new MyThread();
        mt.start();

        // 主线程
        for (int j = 0; j < 100; j++) {
            System.out.println("主线程j的值是:" + j);
        }
    }
}

多线程在内存中的存储机制

3、JMM 内存模型(Java Memory Model, JMM)

3.1、Java 虚拟机规范中定义的一种内存模型

该模型描述了java程序中共享变量的访问规则,以及在JVM中变量存储到内存与从内存中读取到变量的底层细节。

简而言之:
所有的共享变量都存储在主内存中,每一个线程都有自己的工作内存,工作内存是线程隔离的。线程对变量的操作都需要将变量拷贝一份到自己的工作内存中,操作完修改后,再重新写回到主内存。

JMM的样图

4、多线程的安全问题:

4.1、可见性

4.1.1、出现可见性问题的原因:

一条线程对共享变量的修改,其他线程不可见
在这里插入图片描述

4.2、有序性

4.1.2、出现有序性问题的原因:

1、 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:

​             int a = 10;     //1int b = 20;     //2int c = a + b;   //3

第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。12先编译谁,不影响第三行的结果。


2、但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:  

有序性问题

4.3、原子性

4.3.1、什么是原子性:

是指在一次操作或多次操作中,要么所有的操作都得到了执行,并且不会受其他因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可分割的整体。

4.3.2、出现原子性问题的原因:

多条线程对共享变量操作产生覆盖的效果。
在这里插入图片描述

5、解决多线程中的安全性问题

5.1、加锁(Synchronized),使用同步机制

1、加锁可以解决线程中所有的安全性问题

2、它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行

3、synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

5.1.1、同步代码块:

概述:使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问

锁对象:

  • 语法的角度: 锁对象可以是任意类的对象
  • 同步的角度: 多条线程想要实现同步,那么这多条线程使用的锁对象要一致(相同)
案例:解决买票的线程安全问题
public class MyRunnable implements Runnable {

    // 共享变量--被4条线程共享
    int tickets = 100;

    @Override
    public void run() {
        // 线程的任务代码----->卖票
        // 循环卖票,直到没有票为止
        while (true) {
            // 加锁
            synchronized (this){
                // 条件判断
                if (tickets < 1) {
                    break;
                }
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +
                        ":线程正在出售第" + tickets + "张票");
                tickets--;
            }
            // 释放锁
        }
    }
}


public class Test {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable mr = new MyRunnable();
        // 创建并启动4条线程
        new Thread(mr,"窗口1").start();
        new Thread(mr,"窗口2").start();
        new Thread(mr,"窗口3").start();
        new Thread(mr,"窗口4").start();

    }
}

5.1.2、同步方法:

概述: 使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问

锁对象:

  • 非静态同步方法锁对象是: this
  • 静态同步方法锁对象是: 该方法所在类的字节码对象—>类名.class
案例:
public class MyRunnable implements Runnable {

    // 共享变量--被4条线程共享
    int tickets = 100;

    @Override
    public void run() {
        // 线程的任务代码----->卖票
        // 循环卖票,直到没有票为止
        while (true) {
            // 条件判断
            if (sellTickets()) break;
        }
    }

    // 非静态同步方法
    private synchronized boolean sellTickets() {
        if (tickets < 1){
            return true;
        }
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +
                ":线程正在出售第" + tickets + "张票");
        tickets--;
        return false;
    }
}


public class Test {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable mr = new MyRunnable();
        // 创建并启动4条线程
        new Thread(mr,"窗口1").start();
        new Thread(mr,"窗口2").start();
        new Thread(mr,"窗口3").start();
        new Thread(mr,"窗口4").start();

    }
}

5.1.3、Lock锁

概述: 也是一种锁,他比synchronized更加强大,更加面向对象

使用:
1、Lock是一个接口,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作
2、使用Lock就需要使用Lock接口的实现类ReentrantLock

案例:解决买票案例:
public class MyRunnable implements Runnable {

    // 共享变量--被4条线程共享
    int tickets = 100;

    // 创建Lock对象
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 线程的任务代码----->卖票
        // 循环卖票,直到没有票为止
        while (true) {
            // 条件判断
            // 加锁
            lock.lock();
            if (tickets < 1){
                // 释放锁
                lock.unlock();
                break;
            }
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +
                    ":线程正在出售第" + tickets + "张票");
            tickets--;
            // 释放锁
            lock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable mr = new MyRunnable();
        // 创建并启动4条线程
        new Thread(mr,"窗口1").start();
        new Thread(mr,"窗口2").start();
        new Thread(mr,"窗口3").start();
        new Thread(mr,"窗口4").start();

        /*
            注意:
                1.线程锁对象没有释放,线程就不会销毁
                2.子线程没有销毁,主线程就不能结束\销毁
         */

    }
}

5.2、Volatile关键字

概述: 它是一个修饰符,只能用来修饰成员变量

volatile可以解决可见性,有序性问题,但是不能解决原子性问题

作用:
1.被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值
2.被volatile修饰的成员变量,可以保证不会被编译器重排

5.3、原子(Atomic)类

概述: java.util.concurrent.atomic包中提供了很多原子类,这些原子类在多线程的环境下是线程安全的

作用: 可以解决原子性问题,可见性问题,有序性问题

原子类的工作原理(CAS机制):

CAS机制: 比较并交换, 拿刚刚从主内存中获取的值 与 当前主内存中的值进行比较,如果相同,就把自增1后的值跟主内存中的值进行交换,如果不相同,就不交换,而是从新获取主内存中的值,再进行比较并交换,…
CAS机制原理

AtomicInteger类:

  • public AtomicInteger();创建一个AtomicInteger对象,表示整数0
  • public AtomicInteger(int nul);创建一个AtomicInteger对象,表示指定整数
  • public final int getAndIncrement(); 自增1
  • public final int get(); 获取当前对象表示的整数值
案例:
public class MyThread extends Thread {
    // 共享变量
    // volatile static int a = 0;
    static AtomicInteger a = new AtomicInteger(0);

    @Override
    public void run() {
        // 子线程对a自增10万次
        for (int i = 0; i < 100000; i++) {
            a.getAndIncrement();
        }
        System.out.println("子线程操作完毕!");
    }
}

public class Test {
    public static void main(String[] args) throws Exception{
        // 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
        // 创建并启动线程
        MyThread mt = new MyThread();
        mt.start();

        // 主线程对a自增10万次
        for (int i = 0; i < 100000; i++) {
            MyThread.a.getAndIncrement();
        }

        // 为了保证主线程和子线程都执行完毕,再打印最终a的值
        Thread.sleep(3000);

        System.out.println("最终a的值:"+ MyThread.a.get());

    }
}

三、并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

1、CopyOnWriteArrayList线程安全

案例:
public class MyThread extends Thread {
    // 共享变量---ArrayList
    //static ArrayList<Integer> list = new ArrayList<>();
    static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();


    @Override
    public void run() {
        // 线程任务:往集合中添加10000个元素
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        // 案例: 2条线程同时往集合中分别添加10000个元素
        // 创建并启动2条线程
        new MyThread().start();
        new MyThread().start();

        // 为了保证2条集合都操作完毕,再来打印集合元素个数
        Thread.sleep(2000);
        System.out.println("集合元素个数:" + MyThread.list.size());
        /*
            结果: list集合的元素个数一定2万个
         */
    }
}

2、CopyOnWriteArraySet线程安全

public class MyThread extends Thread {
    // 共享变量---HashSet
    //static HashSet<Integer> set = new HashSet<>();
    static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
        // 线程任务:往集合中添加10000个元素
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        // 案例: 2条线程同时往集合中分别添加10000个元素
        // 创建并启动2条线程
        new MyThread().start();
        new MyThread().start();

        // 为了保证2条集合都操作完毕,再来打印集合元素个数
        Thread.sleep(2000);
        System.out.println("集合元素个数:" + MyThread.set.size());
        /*
            结果: set集合的元素个数一定是1万个
         */
    }
}

3、ConcurrentHashMap线程安全

public class MyThread extends Thread {
    // 共享变量---HashSet
    //static HashMap<Integer,Integer> map = new HashMap<>();
    //static Hashtable<Integer,Integer> map = new Hashtable<>();
    static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        // 线程任务:往集合中添加10000个元素
        for (int i = 0; i < 10000; i++) {
            map.put(i,i);
        }
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        // 案例: 2条线程同时往HashMap集合中添加10000个键值对
        // 创建并启动2条线程
        new MyThread().start();
        new MyThread().start();

        // 为了保证2条集合都操作完毕,再来打印集合元素个数
        Thread.sleep(2000);
        System.out.println("集合元素个数:" + MyThread.map.size());
        /*
            结果: Map集合的元素个数一定是1万个

         */
    }
}

3.1、ConcurrentHashMap和HashTable的效率问题:

3.1.1、HashTable效率低下原因:

public synchronized V put(K key, V value)
public synchronized V get(Object key)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
HashTable

3.1.2、ConcurrentHashMap高效的原因:

CAS + 局部(synchronized)锁定
ConCurrentHashMap

4、CountDownLatch

概述:CountDownLatch允许一个或多个线程等待其他线程完成操作。

public class MyThread1 extends Thread {

    CountDownLatch cdl;

    public MyThread1(CountDownLatch cdl) {
        this.cdl = cdl;
    }

    @Override
    public void run() {
        System.out.println("线程1: 打印A....");
        // 打印完A后,进入等待
        try {
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1: 打印C....");
    }
}

public class MyThread2 extends Thread {

    CountDownLatch cdl;

    public MyThread2(CountDownLatch cdl) {
        this.cdl = cdl;
    }

    @Override
    public void run() {
        System.out.println("线程2: 打印B....");
        // 打印完B后,计数器-1
        cdl.countDown();

    }
}


public class Test {
    public static void main(String[] args)throws Exception{
        CountDownLatch cdl = new CountDownLatch(1);
        new MyThread1(cdl).start();
        Thread.sleep(100);
        new MyThread2(cdl).start();
    }
}
// 注意: 线程1和线程2需要使用同一个CountDownLatch对象

5、CyclicBarrier

作用: 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

案例演示:
- 例如: 公司召集5名员工开会,等5名员工都到了,会议开始
public class MyRunnable implements Runnable {

    CyclicBarrier cb;

    public MyRunnable(CyclicBarrier cb) {
        this.cb = cb;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":到达会议室...");
        // 进入线程等待
        try {
            cb.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":离开会议室...");
    }
}


public class Test {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("开始开会....");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("会议结束,大家可以自行离开...");
            }
        });
        // 创建任务对象
        MyRunnable mr = new MyRunnable(cb);
        // 创建5条线程并启动
        new Thread(mr,"员工1").start();
        new Thread(mr,"员工2").start();
        new Thread(mr,"员工3").start();
        new Thread(mr,"员工4").start();
        new Thread(mr,"员工5").start();
    }
}

6、Semaphore

作用: Semaphore的主要作用是控制线程的并发数量。

常用方法:
public Semaphore(int permits) permits 表示许可线程的数量
public void acquire() 表示获取许可
public void release() 表示释放许可

案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
public class HuiSuo {
 Semaphore sp;

 public HuiSuo(Semaphore sp) {
     this.sp = sp;
 }

 public void comeInRoom(){
     // 获得许可证---获取手牌
     try {
         sp.acquire();
     } catch (InterruptedException e) {
         e.printStackTrace();
     }

     // 按摩洗脚
     System.out.println(Thread.currentThread().getName()+":拿到手牌,正在享受18号技师按摩洗脚服务...");
     try {
         Thread.sleep(2000);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }

     System.out.println(Thread.currentThread().getName()+":归还手牌,结束18号技师按摩洗脚服务...");
     // 释放许可证---归还手牌
     sp.release();
 }
 }

public class Test {
    public static void main(String[] args) {
        // 案例: 模拟多条线程进入浴室,但控制每次允许2个人进入浴室
        Semaphore sp = new Semaphore(2);
        // 创建会所对象
        HuiSuo hs = new HuiSuo(sp);
        // 创建5条线程并启动
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 进入浴室
            hs.comeInRoom();
        }
    },"1号顾客").start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            // 进入浴室
            hs.comeInRoom();
        }
    },"2号顾客").start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            // 进入浴室
            hs.comeInRoom();
        }
    },"3号顾客").start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            // 进入浴室
            hs.comeInRoom();
        }
    },"4号顾客").start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            // 进入浴室
            hs.comeInRoom();
        }
    },"5号顾客").start();
}
}

7、Exchanger

作用:是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

常用方法:
public Exchanger()
public V exchange(V x) 参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据

public class MyThread1 extends Thread {

    Exchanger<String> ex;

    public MyThread1(Exchanger<String> ex) {
        this.ex = ex;
    }

    @Override
    public void run() {
        // 任务: 把itheima字符串传给B线程
        System.out.println("A线程:准备把itheima传递给B线程...");
        try {
            String msgB = ex.exchange("itheima");
            System.out.println("A线程: B线程传递过来的数据"+msgB);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class MyThread2 extends Thread {

    Exchanger<String> ex;

    public MyThread2(Exchanger<String> ex) {
        this.ex = ex;
    }

    @Override
    public void run() {
        // 任务: itcast字符串传给A线程
        System.out.println("B线程:准备把itcast传递给A线程...");
        try {
            String msgA = ex.exchange("itcast");
            System.out.println("B线程: A线程传递过来的数据"+msgA);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}


public class Test {
    public static void main(String[] args) {
        // 案例演示:  AB两条线程交换字符串数据
        // eg: A线程: itheima字符串传给B线程
        // eg: B线程: itcast字符串传给A线程
        Exchanger<String> ex = new Exchanger<>();
        // 创建并启动2条线程
        new MyThread1(ex).start();
        new MyThread2(ex).start();
    }
}

四、线程的状态

线程从创建到销毁的过程称为线程的生命周期,在线程的生命周期内一共有六种状态:

线程的状态


java后续基础内容正在快马加鞭更新中,内容若有误,欢迎各位码友提出建设性意见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值