java学习笔记19(线程、进程、多线程、IO)

这篇博客详细介绍了Java中的多线程概念,包括进程、线程的区别,线程调度,以及创建新线程的三种方式。还讨论了同步机制,如同步代码块和同步方法,以及Lock锁。此外,博主分享了Java中的File类及其相关方法,讲解了字节流和字符流的使用,最后给出了几个关于线程和I/O操作的编程任务。
摘要由CSDN通过智能技术生成

学习内容

多线程

进程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

线程

某些进程内部还需要同时执行多个子任务,这些子任务就是线程。

例如:用播放器播放视频时,程序输出视频画面是一个进程、音频是一个进程、字幕是一个进程、显示视频进度是一个进程。

线程是操作系统调度的最小任务单位。

线程与进程

进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。

具体采用哪种方式,要考虑到进程和线程的特点。

和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:

  • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

并发和并行

并行:在同一时刻,在多个指令在多个cpu同时进行。

并发:在同一时刻,在多个指令在单个cpu交替执行。

多线程编程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。

例如,播放视频时时,就必须由三个线程分别播放视频、播放音频、呈现字母,三个线程需要协调运行,否则就会出现如音画不同步、字母不同步等情况。因此,多线程编程的复杂度高,调度更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

因此,必须掌握Java多线程编程才能继续深入学习其他内容。

java 中的线程

线程调度

分时调度

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

抢占式调度

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对较多一些。

创建新线程

Java用Thread对象表示一个线程。

第一种方式,利用 Thread
  1. 创建 Thread 类的子类

  2. 在这个子类中重写 Thread 类的 run() 方法,设置线程任务。

  3. 创建子类的实例对象。

  4. 调用 Thread 类中的 start() 方法,开启这个线程,调用 run() 方法。

注意:一个线程对象只能调用一次 `start()` 方法;

线程的执行代码写在 `run()` 方法中;

线程调度由操作系统决定,程序本身无法决定调度顺序;
第二种方式,实现 Runnable 接口
  1. 定义 Runnable 接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正
    的线程对象。
  3. 调用线程对象的start()方法来启动线程。

Thread.sleep()可以把当前线程暂停一段时间,传入的参数是毫秒。可以通过传参来调整暂停时间的大小。

第三种方式:匿名内部类

线程的优先级

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

同步

同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。
  3. 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程进入阻塞状态等待(BLOCKED)。

同步方法

使用 synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着,进入阻塞状态。

格式:

public synchronized void method(){
   可能会产生线程安全问题的代码 
}

同步方法中的同步锁:

  • 对于非static方法,同步锁就是this。
  • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

Lock 锁机制

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块 / 同步方法具有的功能Lock都有,还有更强大的功能。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock() :加同步锁。

public void unlock() :释放同步锁。

使用步骤:

  1. 先创建Lock对象,

    Lock lock =new ReentrantLock();
    
  2. 在可能出现同步安全问题的代码前调用Lock接口中的方法lock() 获取锁

  3. 在可能出现同步安全问题的代码后调用Lock接口中的方法unlock() 释放锁

线程状态

  • 新建(NEW):新创建了一个线程,但还没调用线程的 start 方法;

  • 运行(RUNNABLE),又包括:

    ​ 就绪(ready):运行线程的 start 方法启动后,线程位于可运行线程池中,等待被调度;

    ​ 运行中(RUNNING):就绪的线程获得 CPU 的时间片就变为运行中。

  • 阻塞(BLOCKED):线程等待获得锁;

  • 等待(WAITING):接收事件通知后或系统中断后进入等待,进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

  • 超时(TIMED_WAITING):等待指定时间后会自行返回;

  • 终止(TERMINATED):线程已执行完毕;

注意:阻塞(BLOCKED)和等待(WAITING)不用刻意区分,这两者都会暂停线程。

I/0

File 类

概述

文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

File 类中的静态成员变量

static String pathSeparator :与系统有关的路径分隔符,用来分隔多个路径,在 windows 中是分号 ;

static String separator :与系统有关的默认名称分隔符,在 widows 中是反斜杠 \

路径

绝对路径:从盘符开始的路径,这是一个完整的路径。

相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。

构造方法

public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。

public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。

public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

常用方法

public String getAbsolutePath() :返回此File的绝对路径名字符串。

public String getPath() :将此File转换为路径名字符串。

public String getName() :返回由此File表示的文件或目录的名称。

public long length() :返回由此File表示的文件的长度。

判断功能的方法

public boolean exists() :此File表示的文件或目录是否实际存在。

public boolean isDirectory() :此File表示的是否为目录。

public boolean isFile() :此File表示的是否为文件。

增删的方法

public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。

public boolean delete() :删除由此File表示的文件或目录。

public boolean mkdir() :创建由此File表示的目录。

public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。

目录的遍历

public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。

public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。

字节流

字节输出流

java.io.OutputStream 类是一个抽象类,表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字
节输出流的基本共性功能方法。

常用方法

public void close() :关闭此输出流并释放与此流相关联的任何系统资源。当完成流的操作时,必须调用此方法,释放系统资源。

public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。

public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。

public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
public abstract void write(int b) :将指定的字节输出流。

FileOutputStream 类

FileOutputStream 类是 OutputStream 类的子类,它是文件输出流,用于将数据输出到文件。

常用方法:

public void close() :关闭此输出流并释放与此流相关联的任何系统资源。

public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。

public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。

public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。

public abstract void write(int b) :将指定的字节输出流。

字节输入流 InputStream

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

public void close() :关闭此输入流并释放与此流相关联的任何系统资源。当完成流的操作时,必须调用此方法,释放系统资源。

public abstract int read() : 从输入流读取数据的下一个字节。

public int read(byte[] b) : 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

FileInputStream 类

该类是文件输入流,从文件中读取字节。

读取字节数据
  1. 读取字节: read 方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回 -1
  2. 使用字节数组读取: read(byte[] b) ,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读
    取到末尾时,返回 -1

字符流

内容跟字节流基本差不多,就不复制粘贴了。

要点:输入流的抽象类是Reader,其读取字符文件的子类是FileReader

输出流的抽象类是Writer,其读取字符文件的子类是FileWrite

读取、输出的时候,把字节流里的byte[]换成char[]即可。

与字节流区别

字符流,只能操作文本文件,不能操作图片,视频等非文本文件。

当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流

任务内容

任务0

运行后可看看效果,然后尝试着解释为什么结果是这样的。

package com.xxm.advanced_camp.mission10_multithreading;

public class test1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁") {
                    System.out.println("t1 start");
                    try {
// t1 释放锁
                        "锁".wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 end");
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁") {
                    System.out.println("t2 start");
                    try {
// 通知 t1 进入等待队列
                        "锁".notify();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2 end");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

答:运行结果

t1 start
t2 start
t2 end
t1 end

答:

尝试解释:

JVM先调用主方法 → 调用 t1 线程的 run() 方法 → 创造一个“同步锁” → 执行输出语句,输出 “t1 start” → 进入等待状态,JVM继续执行主方法 → 调用 t2 线程的 run() 方法 → 执行输出语句,输出 “t2 start” → 通知所有“同步锁”的成员,等待中的线程可以继续执行了 → 执行输出语句,输出 “t2 end” →继续执行“同步锁”成员中的等待成员后面的语句,也就是 t1 线程中后面的语句,也就是执行输出语句,输出"t1 end"。

任务1 熟悉线程生命周期方法

1-1

开启四个线程,两个线程调用锁的 wait 方法,另外两个调用锁的 notify 方法,观察执行结果并解释原因。

答:代码如下 :

package com.xxm.advanced_camp.mission10_multithreading.task1;

/*
开启四个线程,两个线程调用锁的 wait 方法,另外两个调用锁的 notify 方法,观察执行结果并解释原因。
 */
public class Task1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁") {
                    System.out.println("t1 线程开始");
                    try {
                        "锁".wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 线程结束");
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("lock1") {
                    System.out.println("t2 线程开始");

                    try {
                        "lock1".wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2 线程结束");
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁") {
                    System.out.println("t3 线程开始");

                    try {
                        "锁".notify();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("t3 线程结束");
                }
            }
        });

        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("lock1") {
                    System.out.println("t4 线程开始");

                    try {
                        "lock1".notify();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println("t4 线程结束");
                }
            }
        });


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

执行结果并不唯一,有多种情况

//第一次执行
t1 线程开始
t2 线程开始
t3 线程开始
t4 线程开始
t3 线程结束
t4 线程结束
t1 线程结束
t2 线程结束

//第二次执行
t1 线程开始
t2 线程开始
t3 线程开始
t3 线程结束
t1 线程结束
t4 线程开始
t4 线程结束
t2 线程结束

  
//第三次执行
t2 线程开始
t1 线程开始
t4 线程开始
t3 线程开始
t4 线程结束
t3 线程结束
t1 线程结束
t2 线程结束
  

原因:

线程的执行顺序是不确定的。调用Threadstart()方法启动线程时,线程的执行顺序是不确定的。

也就是说,在同一个方法中,连续创建多个线程后,调用线程的start()方法的顺序并不能决定线程的执行顺序。

但是利用wait()notify()方法后,可以保证一定的顺序。

所以在上面的结果中, t1,t2 结束一定不会都在 t3、t4 开始的上面。即:只有当调用了线程 t3 或 t4 中的notify()方法,线程 t1 和 t2 中wait()方法后的语句才会被执行。

1-2

在 main 函数中开启一个子线程,如果想让 main 主线程在子线程执行完之后才继续执行,代码该怎么写?


任务2

使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。

现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。

要求:

用 Thread 类实现;

用 Runnable 接口实现。

Runnable 类

package com.xxm.advanced_camp.mission10_multithreading;

/*
使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。
现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。要求:
用 Thread 类实现;
用 Runnable 接口实现。
 */

public class SellPhones {
    private static int iphones = 100;

    public static void main(String[] args) {


        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized ("lock") {
                        if (iphones > 0) {
                            String threadName = Thread.currentThread().getName();
                            System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
                            iphones--;
                        }
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized ("lock") {
                        if (iphones > 0) {
                            String threadName = Thread.currentThread().getName();
                            System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
                            iphones--;
                        }
                    }
                }
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized ("lock") {
                        if (iphones > 0) {
                            String threadName = Thread.currentThread().getName();
                            System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
                            iphones--;
                        }
                    }
                }
            }
        });

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

Thread 类:

1、先实现 Thread 接口,覆写run()方法

package com.xxm.advanced_camp.mission10_multithreading.task2;

import com.xxm.advanced_camp.mission10_multithreading.task2.SellPhones2;

public class MyThread extends Thread{
    @Override
    public void run(){
        while (true) {
            synchronized ("lock") {
                if (SellPhones2.iphones > 0) {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName + "柜台正在兑换第 " + SellPhones2.iphones + " 台 iPhone");
                    SellPhones2.iphones--;
                }
            }
        }

    }
}

2、创建MyThread类的实例对象,直接start,完成。

package com.xxm.advanced_camp.mission10_multithreading.task2;

/*
使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。
现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。要求:

用 Thread 类实现;
 */


public class SellPhones2 {
    public static int iphones = 100;

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

任务3

使用 newScheduledThreadPool 线程池实现每隔 1 分钟打印一条消息。

答:代码如下:

package com.xxm.advanced_camp.mission10_multithreading.task3;

/*
使用 newScheduledThreadPool 线程池实现每隔 1 分钟打印一条消息。
 */

import java.util.concurrent.*;

public class Task3 {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("一分钟过去了");
            }
        }, 0, 1, TimeUnit.MINUTES);
    }
}

任务4

(1)在 Windows 系统中,以递归方式读取 C 盘中所有的目录和文件,并打印出每个文件的大小和每个目中文件的数量。

答:思路:

1、创建File 对象,

2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。

3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。

package com.xxm.advanced_camp.Mission11_IO;

import java.io.File;

/*
1、创建File 对象,

2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。

3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。

 */
public class Task1 {
    public static void main(String[] args) {
        //1、创建File对象
        File f = new File("C:");
        printAll(f);
    }

    //2、创建遍历打印的方法
    public static void printAll(File file) {

        File[] files = file.listFiles();
        for (File f : files) {
            //判断是否为文件或文件夹
            if (f.isFile()) {
                System.out.println(f.getAbsolutePath());
            } else if (f.isDirectory()) {
                System.out.println(f.getAbsolutePath());
                printAll(f);
            }
        }
    }
}package com.xxm.advanced_camp.Mission11_IO;

import java.io.File;

/*
1、创建File 对象,

2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。

3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。

 */
public class Task1 {
    public static void main(String[] args) {
        //1、创建File对象
        File f = new File("C:");
        printAll(f);
    }

    //2、创建遍历打印的方法
    public static void printAll(File file) {

        File[] files = file.listFiles();
        for (File f : files) {
            //判断是否为文件或文件夹
            if (f.isFile()) {
                System.out.println(f.getAbsolutePath());
            } else if (f.isDirectory()) {
                System.out.println(f.getAbsolutePath());
                printAll(f);
            }
        }
    }
}

任务5

在网络上下载一个大文件(任何文件都可以,可以是 exe 文件,也可以是日志、电影或其它类型的大文件),然后使用 Java 标准 I/O 模型读取并复制成另一份文件:

用字节流实现;

答:

package com.xxm.advanced_camp.Mission11_IO;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

/*
在网络上下载一个大文件(任何文件都可以,可以是 exe 文件,也可以是日志、电影或其它类型的大文件),然后使用 Java 标准 I/O 模型读取并复制成另一份文件:
用字节流实现;
 */
public class Task2 {
    public static void main(String[] args) throws Exception {
        File file = new File("E:\\xxm_C1\\计算机通识-1.mp4");


        //1.指定数据源
        FileInputStream fis = new FileInputStream(file);

        //2.指定输出位置
        FileOutputStream fos = new FileOutputStream("E:\\xxm_C1\\计算机通识的分身1号.mp4");

        //3.读取数据
        byte[] b = new byte[1024];
        int len;
        while ((len = fis.read(b)) != -1) {
            fos.write(b, 0, len);
        }

        //4.释放资源
        fis.close();
        fos.close();
    }
}

用字符流实现:

package com.xxm.advanced_camp.Mission11_IO;

import java.io.FileReader;
import java.io.FileWriter;

public class Task2_2 {
    public static void main(String[] args) throws Exception {
        //1.指定输入源
        FileReader fr = new FileReader("C:\\Users\\yyq\\Desktop\\日报\\10月25日.md");

        //2.指定输出位置
        FileWriter fw = new FileWriter("C:\\Users\\yyq\\Desktop\\日报\\10月25日的日报的分身.md");

        //3.读取数据
        char[] c = new char[1024];
        int len;
        while ((len = fr.read(c)) != -1) {
            fw.write(c, 0, len);
            fw.flush();
        }

        //4.释放资源
        fr.close();
        fw.close();
    }


}

任务6

针对练习 2,用 NIO 重新实现,然后比较一下执行效率。

答:

package com.xxm.advanced_camp.Mission11_IO;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;

public class Task3 {
    public static void main(String[] args) throws Exception {
        FileChannel in = new FileInputStream("C:\\Users\\yyq\\Desktop\\日报\\微型 ORM 框架.md").getChannel(),
                out = new FileOutputStream("C:\\Users\\yyq\\Desktop\\日报\\微型 ORM 框架的 NIO 分身.md").getChannel();
        in.transferTo(0, in.size(), out);
    }
}

复制同一个文本文件时,字符流和 NIO 执行时间差不多,但有可能是因为文本文件太小,导致产生了天花板效应。

复制同一个视频文件(1.58 G)时,字节流耗时约为 37520 毫秒;NIO 方法约为 36902 毫秒,好像差不多的样子。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值