day18【死锁、线程状态、等待与唤醒、Lambda】

今日内容

  • 死锁
  • 线程状态
  • 等待与唤醒

教学目标

  • 能够描述死锁产生的原因
  • 能够说出线程6个状态的名称
  • 能够理解等待唤醒案例

第一章 死锁

1. 什么是死锁

死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

死锁在开发中,也会遇到,当线程进入到死锁状态时,程序中线程就会一直处于等待状态。

死锁的发生:

举例1:

我请柳岩吃饭(麻辣烫)

我点了一份。上了一双筷子

我:一支

柳岩:一支

我和柳岩就相当于两个线程,而麻辣烫相当于共享资源,两个人都没办法吃,都在等待看着麻辣烫,这种现象就是死锁。

举例2:有两个线程(t1线程、t2线程),有两个对象锁(lock_a、lock_b)

​ t1线程在执行时,先拿到lock_a对象锁(此时lock_a对象锁绑定在t1线程上)。而正在此时CPU切换到t2线程上,t2线程拿到lock_b对象锁(此时lock_b对象锁绑定在t2线程上),这时CPU又切换到t1线程上,这时t1线程需要拿lock_b对象锁,此时t1线程获取不到lock_b对象锁(t1线程处于等待)。

当CPU切换到t2线程上,这时t2线程需要拿lock_a对象锁,此时t2线程获取不到lock_a对象锁(B线程处于等待)。

上述案例2简化如下说法:

​ 有2个线程,需要执行相同的任务,但是需要分别获取的A锁和B锁才能去执行,第一个线程获取锁的顺序是先A后B。第二个线程获取锁的顺序是先B后A。

当第一个线程获取A锁,CPU切换到第二个线程,此时第二个线程获取B锁。而此时第一个线程缺少B锁,第二个线程缺少A锁。两个线程都在等待,发生死锁现象。

2.产生死锁的条件

  1. 有多把锁

  2. 有多个线程

  3. 有同步代码块嵌套

3. 代码演示

分析和步骤:

1)创建一个任务类DeadLockTask 实现Runnable接口,复写run函数;

2)创建两个Object类的对象lock_a,lock_b作为锁对象;

3)定义一个变量flag,让不同的线程切换到不同的地方去执行,按照不同的方式来获取锁;

4)在run函数中使用if-else结构来控制两个线程去执行不同的内容,并使用while循环一直让其执行;

5)在if中嵌套书写两个同步代码块lock_a和lock_b分别作为两个代码块的锁,将if中相同的内容复制一份写到else中;

6)创建测试类DeadThreadLockDemo,在这个类的主函数中创建任务类的对象;

7)创建两个线程对象t1和t2;

8)让主线程休息1毫秒;

9)使用t1对象调用start函数开启线程,让下一个线程进入到else中;

10)开启t2线程;

代码如下所示:

/*
 * 演示线程死锁的问题
 */
//定义一个线程任务类
class DeadLockTask implements Runnable
{
	//定义两个锁对象
	private Object lock_a=new Object();
	private Object lock_b=new Object();
	//定义一个变量作为标记,控制取锁的方式
	 boolean flag=true;
	public void run() {
		//当线程进来之后,一个线程进入到if中,另一个进入到else中
		if(flag)
		{
			while(true)
			{
				synchronized(lock_a)
				{
					System.out.println(Thread.currentThread().getName()+"if.....lock_a");
					synchronized(lock_b)
					{
						System.out.println(Thread.currentThread().getName()+"if.....lock_b");
					}
				}
			}
		}else
		{
			while(true)
			{
				synchronized(lock_b)
				{
					System.out.println(Thread.currentThread().getName()+"else.....lock_b");
					synchronized(lock_a)
					{
						System.out.println(Thread.currentThread().getName()+"else.....lock_a");
					}
				}
			}
		}
	}
}
public class DeadThreadLockDemo {

	public static void main(String[] args) {
		// 创建任务类对象
		DeadLockTask dlt = new DeadLockTask();
		//创建线程对象
		Thread t1 = new Thread(dlt);
		Thread t2 = new Thread(dlt);
		//开启第一个线程
		t1.start();
		//修改标记让下一个线程进入到else中
		dlt.flag=false;
		t2.start();
	}
}

上述代码的结果如下图所示:

在这里插入图片描述

通过以上结果发现,线程一直在执行else中的代码,根本就没有执行if语句中的代码。

说明:

出现上述结果是因为在主线程开启第一个线程之后,很有可能CPU还在主线程上运行,那么开启的线程是不会被CPU立刻去执行,而CPU继续处理主线程中的代码, 就会直接去执行d.flag = false; ,这时就已经把标记修改成false,不管线程是否进入到run 方法中,flag都已经变成false,那么就无法在进入if中,因此我们为了保证第一个线程一定能够进入到if中,于是在这里让主线程在开启第一个线程之后,主线程进行休眠1毫秒。

在这里插入图片描述

死锁的结果如下图所示:

在这里插入图片描述

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。

​ 在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。

​ 可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)

第二章 线程状态

线程状态概述

线程由生到死的完整过程:技术素养和面试的要求。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典叫法)
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

在这里插入图片描述

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

第三章 等待唤醒机制(包子铺卖包子)

​ 我们的卖票案例中,所有的线程都是在做相同的任务:卖票。而我们真实情况中,不同的线程有可能需要去做不同的任务。在程序中,最常见的一种模型,就是生产者和消费者模型。生产者线程和消费者线程之间需要进行通信,我们可以使用等待唤醒机制来实现生产者线程和消费者线程之间的通信。

  • Object类的方法

    wait()		:让当前线程进入等待状态
    notify()	:唤醒一个正在等待的线程,唤醒是随机的
    void notifyAll() 唤醒在此对象监视器上等待的所有线程。 
    
    注意事项: 必须要使用锁对象来调用的。
    
    • 两个方法的小疑问

      • 等待和唤醒的方法为什么要定义在Object类中?

        因为需要用锁对象调用这两个方法,任意对象都可以作为锁对象。
        也就是说任意类型的对象都可以调用的两个方法,就需要定义在Object类中
        
      • 两个方法必须写在同步里面吗?

        两个方法必须要在同步里面调用,因为在同步里面才有锁对象。
        
    • 如果一个线程执行了wait()方法,那么当前线程进入等待状态,并且会释放锁对象,下次即使被唤醒必须获取到锁对象才可以执行。

    代码演示:

    public class MyRun1 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                try {
                    //等待
                    "abc".wait();
                } catch (InterruptedException e) {
                }
            }
            System.out.println(1);
            System.out.println(2);
        }
    }
    
    public class MyRun2 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                //notify()也必须要用锁对象调用
                "abc".notify();
    
                System.out.println("A");
                System.out.println("B");
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) throws Exception{
            //只能用锁对象调用  别的对象调用就会报错
    //        "abc".wait();
    
            //开启线程
            MyRun1 mr = new MyRun1();
            new Thread(mr).start();
    
            //睡一秒钟
            Thread.sleep(1000);
    
            MyRun2 mr2 = new MyRun2();
            new Thread(mr2).start();
        }
    }
    
  • 包子案例

    说明:

    ​ 1.定义一个包子类,类中成员变量:

     pi //皮儿
     xian //馅儿
     flag://用来表示有没有包子,true来代表有   用false来代表没有
    

    ​ 2.定义一个生产包子的任务类即生产者线程类:

    生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
    

    ​ 3.定义一个消费包子的任务类即消费者线程类:

    消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
    
    /*
        包子类需要定义3个成员变量:
            pi
            xian
            flag:表示是否有包子
     */
    //包子类
    public class BaoZi {
        //皮儿
        String pi;
        //馅儿
        String xian;
        //布尔值
        boolean flag=false;  //用来表示有没有包子,用true来代表有   用false来代表没有
    }
    
    //生产包子:生产者线程执行的任务
    /*
        生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
     */
    public class ZhiZuo implements Runnable {
        //成员变量
        BaoZi baoZi;
        //构造方法
        public ZhiZuo(BaoZi baoZi) {
            this.baoZi = baoZi;
        }
    
        @Override
        public void run() {
            //制作包子
            while (true){
                synchronized ("锁"){//t1
                    if(baoZi.flag == true){
                        //如果有包子就不需要制作
                        //就让制作的线程进入等待状态
                        try {
                            "锁".wait();
                        } catch (InterruptedException e) {
                        }
                    }
                        //表示没有包子
                        //制作包子
                        baoZi.pi = "白面";
                        baoZi.xian = "韭菜大葱";
                        //修改包子状态
                        baoZi.flag = true;
                        System.out.println("生产出了一个包子!");
    
                        //生产好了包子叫醒吃货(消费者)来吃
                        "锁".notify();
                 
                }
            }
        }
    }
    
    //吃包子:消费者线程执行的任务
    /*
        消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
     */
    public class ChiHuo implements Runnable {
        //成员变量
        BaoZi baoZi;
        //构造方法
        public ChiHuo(BaoZi baoZi) {
            this.baoZi = baoZi;
        }
    
        @Override
        public void run() {
            //吃包子
            while(true){
                synchronized ("锁"){
                    if(baoZi.flag == false){
                        //没包子
                        //让吃包子的线程进入等待
                        try {
                            "锁".wait();
                        } catch (InterruptedException e) {
                        }
                    }
                        //表示有包子
                        //开吃
                        System.out.println("吃货吃了一个" + baoZi.pi+"皮儿," + baoZi.xian + "馅儿的大包子");
                        baoZi.pi = null;
                        baoZi.xian = null;
                        //修改包子状态
                        baoZi.flag = false;
    
                        //吃完包子叫醒对方(生产者)来做
                        "锁".notify();
                    
                }
            }
        }
    }
    
    //测试类
    public class Test01 {
        public static void main(String[] args) {
            //创建包子
            BaoZi baoZi = new BaoZi();
            //创建对象
            ZhiZuo zz = new ZhiZuo(baoZi);
            Thread t1 = new Thread(zz);//生产者线程
            t1.start();
            //创建对象:消费者线程
            ChiHuo ch = new ChiHuo(baoZi);
            Thread t2 = new Thread(ch);
            t2.start();
        }
    }
    

第四章 定时器Timer

  • 功能介绍

    ​ 定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件 事情。定时器类是java.util.Timer类

  • 方法介绍

    构造方法:
    	public Timer():构造一个定时器
    	
    常用方法:
    void schedule(TimerTask task, long delay)//在指定的延迟之后安排指定的任务执行。
        参数:
            task - 所要安排的任务。属于TimerTask类型,java.util.TimerTask是一个实现Runnable接口的抽象类,代表一个可以被Timer执行的任务。我们需要扩展这个类来创建我们自己的TimerTask,它可以使用java Timer类进行调度。
            delay - 执行任务前的延迟时间,单位是毫秒。就是过了多少毫秒之后再执行task任务 
    
    void schedule(TimerTask task, long delay, long period)
    								//在指定 的延迟之后开始,重新执行固定延迟执行的指定任务。
        参数:
            task - 所要安排的任务。
            delay - 执行任务前的延迟时间,单位是毫秒。
            period - 执行各后续任务之间的时间间隔,单位是毫秒。 就是每隔多长时间执行一次task任务
    void schedule(TimerTask task, Date time) //在指定的时间安排指定的任务执行。
        参数:
            task - 所要安排的任务。
            time - 执行任务的时间。 时间一到就会开始执行任务
    
    void schedule(TimerTask task, Date firstTime, long period)
    								//从指定的时间开始,对指定的任务执行重复 的 固定延迟执行 。
         参数:
            task - 所要安排的任务。
            time - 执行任务的时间。 时间一到就会开始执行任务
     		period - 执行各后续任务之间的时间间隔,单位是毫秒。 就是每隔多长时间执行一次task任务
    
  • 代码演示

public class Test01 {
    public static void main(String[] args) throws ParseException {
        //构造方法:
        //public Timer():构造一个定时器
        Timer t = new Timer();

        //常用方法:
        TimerTask tt = new TimerTask() {
            @Override
            public void run() {
                //这里面要写的是执行的代码
                System.out.println("起床了");
            }
        };
        //void schedule(TimerTask task, long delay)
        //在3秒钟之后会自动执行任务
//        t.schedule(tt,3000);

        //void schedule(TimerTask task, long delay, long period)
        //在3秒钟之后会自动执行任务,每过1秒执行一次
//        t.schedule(tt,3000,1000);

        String s = "2020-4-9 12:06:40";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //解析:能够包字符串类型变成Date类型
        Date date = sdf.parse(s);
        //void schedule(TimerTask task, Date time)
        //表示时间到2020-4-9 12:06:40的时候就开始执行任务tt
//        t.schedule(tt,date);
//
//        //void schedule(TimerTask task, Date firstTime, long period)
//        //从指定的时间开始,对指定的任务执行重复 的 固定延迟执行 。
        //表示时间到2020-4-9 12:06:40的时候就开始执行任务tt,以后每隔2秒执行一次任务tt
        t.schedule(tt,date,2000);

    }
}

第五章 1.8特性_ Lambda表达式

5.1 函数式编程思想概述

在这里插入图片描述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

做什么,而不是怎么做

例如之前的匿名内部类方式实现多线程案例中。

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

5.2 Lambda的优化

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。

传统写法,代码如下:

public class Demo03Thread {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多线程任务执行!");
			}
		}).start();
	}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析:

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

Lambda表达式写法,代码如下:

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class Demo04LambdaRunnable {
	public static void main(String[] args) {
		new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
	}
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

5.3 Lambda的格式

标准格式:

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称,参数类型 参数名称,..) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

匿名内部类与lambda对比:

new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多线程任务执行!");
			}
}).start();

仔细分析该代码中,Runnable接口只有一个run方法的定义:

  • public abstract void run();

即制定了一种做事情的方案(其实就是一个方法):

  • 无参数:不需要任何条件即可执行该方案。
  • 无返回值:该方案不产生任何结果。
  • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda语法中,要更加简单:

() -> System.out.println("多线程任务执行!")
  • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。

参数和返回值:

下面举例演示java.util.Comparator<T>接口的使用场景代码,其中的抽象方法定义为:

  • public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String nameint age两个成员变量:

public class Person { 
    private String name;
    private int age;
    
    // 省略构造器、toString方法与Getter Setter 
}

传统写法

如果使用传统的代码对Person[]数组进行排序,写法如下:

public class Demo05Comparator {
    public static void main(String[] args) {
      	// 本来年龄乱序的对象数组
        Person[] array = { new Person("古力娜扎", 19),new Person("迪丽热巴", 18),       		new Person("马尔扎哈", 20) };

      	// 匿名内部类
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

  • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;
  • 为了指定compare的方法体,不得不需要Comparator接口的实现类;
  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 实际上,只有参数和方法体才是关键

Lambda写法

public class Demo06ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
          	new Person("古力娜扎", 19),
          	new Person("迪丽热巴", 18),
          	new Person("马尔扎哈", 20) };

        Arrays.sort(array, (Person a, Person b) -> {
          	return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

省略格式:

省略规则

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参数,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

备注:如果省略大括号、return关键字及语句分号的原则要省略都省略,要么都不能省略。

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());

5.4 Lambda的前提条件

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有接口作为方法参数。
    也就是方法的参数必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

5.5使用lambda总结

匿名内部类:
	可以用于类也可以用于接口,对类和接口中的方法的个数没有要求。
	
Lambda表达式:
	只能用于接口,接口中抽象方法只能有一个。
	
Lambda表达式的要求更严格,并不是所有的匿名内部类都能改成Lambda表达式。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

娃娃 哈哈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值