多线程

本文详细介绍了Java中线程的创建与使用,包括继承Thread类和实现Runnable接口两种方式,以及线程的优先级、同步机制、死锁问题和线程通信。此外,还讨论了Callable接口的使用以及JDK5.0引入的线程池和Lock锁的概念,旨在帮助读者深入理解Java多线程编程。
摘要由CSDN通过智能技术生成

进入高级编程部分(视频406)
在这里插入图片描述

基本概念:程序、进程、线程

概念(视频415、416、417)

进程与线程
单核CPU和多核CPU
并行与并发
多线程的优点、何时需要多线程
	

1、程序(program):用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

2、进程(process):是程序的一次执行过程,是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
进程作为资源分配的单位
一个进程对应有一个方法区和一个堆

3、线程(thread):线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)
若一个进程同一时间并行执行多个线程,就是支持多线程的
一个线程对应有一个虚拟机栈和一个程序计数器。 所有线程共享对应进程的方法区和堆。

线程的创建和使用(API 2种)(重点)

官方文档内容

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of 
Thread. This subclass should override the run method of class Thread. An instance of the subclass can 
then be allocated and started. For example, a thread that computes primes larger than a stated value 
could be written as follows: 

The other way to create a thread is to declare a class that implements the Runnable interface. 
That class then implements the run method. An instance of the class can then be allocated, 
passed as an argument when creating Thread, and started. 
The same example in this other style looks like the following: 

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现

线程的创建和启动

每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
构造器

Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
多线程的创建方式
1、继承Thread
2、实现Runable(平常使用)
=========================================================================================================
这两种方式都是围绕着Thread和Runnable
继承Thread类把run()写到类中
实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装
两种方式最终都是调用Thread类的start()方法来启动线程的,两种方式在本质上没有明显的区别

在外观上有很大的区别,第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,
在继承上有一点受制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式

3、实现Callable
重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread
Callable:有返回值的线程,能取消线程,可以判断线程是否执行完毕
=========================================================================================================
Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以
借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行

当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,
一般通过Thread类来启动线程
Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,
所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现


方式一:继承Thread类
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

1) 定义子类继承Thread类。
2) 子类中重写Thread类的run方法----------->将此线程执行的操作声明在run()中
3) 创建Thread类的子类的对象,即创建了线程对象。
4) 通过此对象调用start方法:启动线程,调用run方法。
API文档中的示例

计算大于某一规定值的质数的线程可以写成: 
	class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }
 
         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }

然后,创建并启动一个线程:
	PrimeThread p = new PrimeThread(143);
    p.start();

代码实现:

创建多线程方式一:继承Thread类(视频418)
创建过程中的两个问题(视频419)
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数(视频420)

遍历10以内的所有的偶数

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class Thread1 {
    public static void main(String[] args) {
        //3.创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4.通过此对象调用start():(1)启动当前线程(2)调用当前线程的run()
        t1.start();

        //问题一:我们不能通过直接调用run()的方式启动线程。
//        t1.run();

        //问题二:再启动一个线程,遍历10以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
        //t1.start();    //异常 IllegalThreadStateException

        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main线程中执行的
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()***********");
            }
        }

    }

控制台输出:

main:0***********main()***********
Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-1:0
Thread-1:2
Thread-1:4
Thread-1:6
Thread-1:8
main:2***********main()***********
main:4***********main()***********
main:6***********main()***********
main:8***********main()***********


}
线程的常用方法(视频421)
package atguigu.Tset;

测试Thread中的常用方法:
 (常用)1. start():启动当前线程;调用当前线程的run()
 (常用)2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
 (帅气)3. currentThread():静态方法,返回执行当前代码的线程
 4. getName():获取当前线程的名字
 5. setName():设置当前线程的名字         通过构造器也可以
				public HelloThread(String name){
        				super(name);
   				 }
				HelloThread h1 = new HelloThread("Thread:1");

 6. yield():释放当前cpu的执行权
 (帅气)7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。

 (已过时)8. stop():当执行此方法时,强制结束当前线程。
 (帅气)9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
10. isAlive():判断当前线程是否存活

11. public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用

myThread.setDaemon(true);       用户线程结束后,只剩下守护线程(gc线程和myThread线程)

运行结果:
main:0
main:1
main:2
main:3
main:4
线程1:0
线程1:1
线程1:2


public class Thread2 {
    public static void main(String[] args) throws InterruptedException{
        MyThread1 myThread1 = new MyThread1("MyThread1");
        myThread1.start();


        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            if (i == 2){
                myThread1.join();
            }
        }
        //myThread1.join();      线程myThread1早已执行完了,返回false
        System.out.println(myThread1.isAlive());
        
    }
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(MyThread1.currentThread().getName() + ":" + i);
            }

            if (i % 3 == 0) {
                yield();
            }
        }
    }

    //通过构造器设置当前线程名字
    public MyThread1(String name) {
        super(name);
    }
}

线程的优先级(视频422)(代码在上面代码 的基础上改进)
1.
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5  -->默认优先级

2.如何获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。
并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
public class Thread2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread1 myThread1 = new MyThread1("MyThread1");

        //设置分线程的优先级:在start()之前
        //从执行多次的概率上讲优先而已,实际不一定
        myThread1.setPriority(Thread.MAX_PRIORITY);
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        myThread1.start();


        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getPriority() + "\t" + i);
            }
            /*if (i == 2) {
                myThread1.join();
            }*/
        }
        //myThread1.join();      线程myThread1早已执行完了,返回false
//        System.out.println(myThread1.isAlive());

    }
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                /*try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                System.out.println(MyThread1.currentThread().getName() + "\t" + getPriority() + "\t" + i);
            }

            if (i % 3 == 0) {
                yield();
            }
        }
    }

    //通过构造器设置当前线程名字
    public MyThread1(String name) {
        super(name);
    }
}

控制台输出:
主线程	1	0
MyThread1	10	0
主线程	1	2
MyThread1	10	2
主线程	1	4
MyThread1	10	4
MyThread1	10	6
MyThread1	10	8
主线程	1	6
主线程	1	8
方式二:实现Runnable接口(视频424)
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。
然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
采用这种风格的同一个例子如下所示:
	class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }
 
         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }
然后,创建并启动一个线程:
	PrimeRun p = new PrimeRun(143);
    new Thread(p).start();

1. 创建一个实现了Runnable接口的类
2. 实现类去实现Runnable中的抽象方法:run()
3. 创建实现类的对象
4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5. 通过Thread类的对象调用start()
创建多线程方式2:实现Runnable接口

public class Thread3 {
    public static void main(String[] args) {
        MyThread3 myThread3 = new MyThread3();

        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        //有一个构造方法:Thread(Runnable target)   分配新的 Thread 对象

        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        new Thread(myThread3).start();      //多态

        //再启动一个线程
        Thread t2 = new Thread(myThread3);
        t2.setName("线程2");
        t2.start();

    }
}

//创建一个实现了Runnable接口的类
class MyThread3 implements Runnable {
    //实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 60; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
        }
    }
}

控制台输出:(只列出一部分)

Thread-0	0
线程2	0
Thread-0	2
线程2	2
Thread-0	4
Thread-0	6
线程2	4
线程2	6
......
......

继承方式和实现方式的联系与区别

 * 比较创建线程的两种方式。
 * 开发中:优先选择:实现Runnable接口的方式
 * 原因:1. 实现的方式没有类的单继承性的局限性
 *      2. 实现的方式更适合来处理多个线程有共享数据的情况。
 *
 * 联系:public class Thread implements Runnable
 * 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

线程的生命周期(视频430)

在这里插入图片描述

线程的同步(安全问题)(重点、难点)

方式一:同步代码块 synchronized ( 同步监视器 ) { 需要同步的代码 }
在java中,我们通过同步机制,来解决线程的安全问题

方式一:同步代码块
synchronized(同步监视器){
	//需要同步的代码
}

说明:操作共享数据的代码,即为需要被同步的代码。(不能包含代码多了,也不能包含代码少了)
			共享数据:多个线程共同操作的变量。比如ticket就是共享数据(安全问题的前提)
			同步监视器:俗称锁。任何一个类的对象,都可以充当锁。(要求:多个线程必须要共用同一把锁)

两个难点:同步监视器、共享数据

==================================================================================================
使用同步代码块解决		实现Runnable的线程		的安全问题(视频433==================================================================================================
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。		

class Window1 implements Runnable {
    private int ticket = 100;       //共享数据,多个线程共同操作的变量
    //    Object obj = new Object();
    Dog dog = new Dog();

    @Override
    public void run() {
//        Object obj = new Object();
        while (true) {

//            synchronized (this){//此时的this:唯一的Window1的对象   //方式二:synchronized (dog) {
            synchronized (dog) {     //核心要点:同步监视器(锁):任何一个类的对象
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);

                    ticket--;
                } else {
                    break;
                }
            }
            
            
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Dog {

}
==================================================================================================
使用同步代码块解决		继承Thread类的方式的线程		的安全问题(视频434==================================================================================================
class Window2 extends Thread{
    private static int ticket = 100;   //造了多个对象,必须是类变量才可以,加上static
    private static Object obj = new Object();

    @Override
    public void run() {

        while(true){
            //正确的
//            synchronized (obj){
            //涉及反射的知识
            synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
                //错误的方式:this代表着t1,t2,t3三个对象
//              synchronized (this){

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
            

        }

    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();
        
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

结果:(只显示部分)
窗口1:卖票,票号为:100
窗口1:卖票,票号为:99
窗口1:卖票,票号为:98
窗口1:卖票,票号为:97
窗口1:卖票,票号为:96
窗口1:卖票,票号为:95
......
......
方式二:同步方法:private (static) synchronized void show(){ … }
方式二:同步方法
关于同步方法的总结:
1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2. 非静态的同步方法,同步监视器是:this
     静态的同步方法,同步监视器是:当前类本身	 (比如Window4.class==================================================================================================
使用同步方法解决	实现Runnable接口的线程的	安全问题
==================================================================================================
class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {

            show();
        }
    }
    
    //核心操作:抽取为方法后    声明为synchronized的
    private synchronized void show(){//同步监视器:this
        //synchronized (this){

            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);

                ticket--;
            }
        //}
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}


==================================================================================================
使用同步方法处理	继承Thread类的方式中的线程的	安全问题
==================================================================================================
class Window4 extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {

        while (true) {

            show();
        }

    }

    //核心操作:方法声明为静态static的(因为同步监视器不统一,有3个)
    private static synchronized void show() {//同步监视器:Window4.class
        //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //(个人添加的说明)处理:方法声明为静态后,用类去调getName()方法
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}
线程的死锁问题(视频438)
1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2.说明:
 * 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
 * 2)我们使用同步时,要避免出现死锁。

存在线程安全问题:重复票和错票

解决线程安全问题的方式三:Lock锁 — JDK5.0新增
1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    
2.调用锁定方法:lock()
    lock.lock();    
    
3.调用解锁方法:unlock()
    lock.unlock();

class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法:lock()
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
1. 面试题:synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器——————自动释放同步监视器
  	 Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())————————手动启动和结束同步(更加灵活)
  	 
2.优先使用顺序:
Lock -----> 同步代码块(已经进入了方法体,分配了相应资源) -----> 同步方法(在方法体之外)

面试题:如何解决线程安全问题?有几种方式

线程的通信(几个方法的使用,难度低于同步)(视频441开始)

wait()、notify()、notifyAll()
(视频441)
线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印		

涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(锁)。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。


===========================================================================================
说明:
 * 1.wait()notify()notifyAll()三个方法        必须使用在同步代码块或同步方法中。(Lock中不可以)
 * 2.wait()notify()notifyAll()三个方法        的调用者必须是同步代码块或同步方法中的同步监视器。
 *    否则,会出现IllegalMonitorStateException异常
 * 3.wait()notify()notifyAll()三个方法        是定义在java.lang.Object类中。(而非Thread中)

(视频442)
面试题:sleep()wait()的异同?
 * 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
 * 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
 *          2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
 *          3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

更新中,累(太菜了)

经典例题:生产者\消费者问题

JDK5.0新增线程创建方式(新增加2种)

新增方式一:实现Callable接口
public class CallableTest {
    public static void main(String[] args) throws Exception {
        // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t=\t" + sum);
    }
}


class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t"
                + Thread.currentThread().getId() + "\t"
                + new Date()
                + " \tstarting...");

        int sum = 0;
        for (int i = 0; i <= 10; i++) {
            sum += i;
        }
        Thread.sleep(5000);

        System.out.println(Thread.currentThread().getName() + "\t"
                + Thread.currentThread().getId()
                + "\t" + new Date()
                + " \tover...");
        return sum;
    }
}
//输出结果
/*
Thread-0	12	Wed Feb 24 14:16:05 CST 2021 	starting...
Thread-0	12	Wed Feb 24 14:16:10 CST 2021 	over...
main	1	=	55
 */

新增方式二:使用线程池
在这里插入代码片
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lacrimosa&L

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

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

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

打赏作者

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

抵扣说明:

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

余额充值