多线程快速入门

多线程

1. 概念

1.1 并发与并行

  • 并发:多个任务在同一个时刻点同时执行。效率高;CPU一核,模拟多条线程,快速交替。

  • 并行:多个任务在同一段时间内分时执行。效率低,宏观上同时执行,微观上分时执行。CPU多核,多个线程同时执行,线程池。

在这里插入图片描述
在这里插入图片描述
注意:并发编程的本质是更加充分的运用CPU的资源。

1.2 进程与线程

  • 进程:内存中正在运行的应用程序,是系统进行资源分配和调度的基本单位。每一个进程有自己独立的运行空间,相互之间不影响。进程就是程序的一次执行过程,即是一个进程从加载到内存到从内存中释放消亡的过程。
  • 线程:进程内部的独立运行单元,是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

就是说:处于运行状态的程序可称为进程,每个进程运行在自己的空间中,空间相对独立,受操作系统保护,在每个进程空间中,一般都会有一个或者多个线程在运行。

  • 多线程:在一个进程中,可以开启多个线程,多个线程同时去执行功能。
  • 主线程:任何一个程序的运行,都有一个独立的运行入口。而负责这个入口的线程称为程序运行的主线程。Java程序的主线程即main线程。

1.3 线程的调度

  • Java 程序的进程里面至少包含两个线程,一个是主进程即 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
  • 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
  • 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

线程的调度方式分两类: 分时调度 , 抢占式调度(时间片轮转)。
分时调度:多个任务平均分配执行时间。
抢占式调度:线程之间抢夺CUP的执行权,谁抢到谁执行(随机性)。

2. 创建线程

2.1 【继承Thread类,重写run方法】

好兄弟,你首先要创建一个类并继承【Thread】类

public class MyThread extends Thread{
 	for(int i = 0; i <10; i++) {
    	System.out.println(i + "-----------"+ Thread.currentThread().getName());
    }
}

创建一个测试类,首先创建线程对象,再开启线程

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //1.创建线程对象
        MyThread myThread = new MyThread();
        //为线程起名
        myThread.setName("线程A");
    
        //2.开启线程-----当获取cpu时间片,那么此线程就会执行run()方法的代码
        myThread.start();
         
        for (int j = 0; j < 10; j++) {
            System.out.println(j + "=============线程名:" + Thread.currentThread().getName());
        }
    }
}

最后,运行能看到
在这里插入图片描述
示例:用线程Thread类实现4个窗口各卖票20张(模拟线程不安全)

public class ThreadTicket extends Thread{
    //票数
    private int ticket = 20;
    //构造函数
    public ThreadTicket (String name) {
        super(name);
    }
    //重写run方法
    @Override
    public void run() {
        while (ticket>0) {
            ticket--;
            System.out.println(Thread.currentThread().getName() + "卖了:" + ticket + "张");
        }
    }
}

class Test01{
    public static void main(String[] args) {
        ThreadTicket a = new ThreadTicket("线程A");
        ThreadTicket b = new ThreadTicket("线程B");
        ThreadTicket c = new ThreadTicket("线程C");
        a.start();
        b.start();
        c.start();
    }
}

2.2 【实现Runnable接口】

嘿嘿,这个你要先来一个类来实现【Runnable】接口,并重写run()方法

//它是一个线程任务类对象
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+">>>>>>"+i);
        }
    }
}

再整一个测试类,

public class Test {
    public static void main(String[] args) {
        //1.创建线程任务类对象
        MyRunnable myRunnable = new MyRunnable();
        //2.创建线程对象
        Thread t1 = new Thread(myRunnable,"线程A");
        //3.开启线程
        t1.start();

		//此处for循环代表主线程
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"<<<<<<<"+i);
        }
    }
}

最后,运行看效果
在这里插入图片描述

2.3 实现Callable接口

创建一个实现Callable的实现类,设置泛型,指定call方法返回的类型

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0){
                sum += i;
            }
        }
        return sum;
    }
}

创建Callable接口实现类的对象

public class Test {
    public static void main(String[] args) {
        //创建Callable接口实现类的对象
        MyCallable callable = new MyCallable();
        //将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
        //获取Callable中call方法的返回值(因为会等待线程结束后再获取,所以可以当作闭关锁使用)
        //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
        try {
            Integer i = futureTask.get();
            System.out.println("i = " + i);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
}

3. 线程状态

在这里插入图片描述

  • New(新创建): 线程被 New出来,但还没调用 start 方法时,就处于这种状态。一旦调用了 start 方法也就进入了 Runnable状态。
  • Runnable(可运行): 处于 Runnable的线程,比较特殊。它还分两种状态:Running 和 Ready。也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
  • 注意: 一个处于 Runnable 状态的线程,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,则该线程暂时不运行。但是,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
  • Blocked(被阻塞): 从 Runnable 进入 Blocked 只有一种可能:就是进入 synchronized 关键字保护的代码,但是没有获取到 monitor 锁。
  • Waiting(等待): 进入该状态表示当前线程需要等待其他线程做出一些的特定的动作(通知或中断)。Waiting 是在等待某个条件,比如 join 的线程执行完毕,或者是 notify ()/notifyAll ()。
  • Timed Waiting(计时等待): 这种状态与 Waiting 状态的区别在于:有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。
  • Terminated(被终止): 最后一种,想要进入终止状态就比较简单了,有三种情况:
    – 任务执行完毕,线程正常退出。
    –出现一个没有捕获的异常(比如直接调用 interrupt () 方法)。

总结:线程的生命周期

  1. 新建:创建线程对象; > .start() >进入就绪状态
  2. 就绪:有执行资格(有资格去抢时间片),没有执行权(还没有抢到不能执行代码),这个阶段就是不停的抢CPU;> 抢到CPU的执行权 > 进入运行状态
  3. 运行:有执行资格,有执行权,这个阶段就是运行代码;但是CPU的执行权有可能被其他线程抢走,抢走后就会回到就绪状态; > run()执行完毕后 > 进入死亡状态
  4. 死亡:线程死亡,变成垃圾。
  5. 阻塞:线程运行时,除了进入死亡状态,还有可能遇到sleep()或者其他阻塞方式,这时就会进入阻塞状态,此时线程没有执行资格,也没有执行权,等sleep()时间或其他阻塞方式结束,就会回到就绪状态

常见方法

名称方法
休眠.sleep(long millis)当前线程主动休眠millis毫秒
加入.join()允许其他线程加入到当前线程中,知道其他线程执行完后,当前线程才会执行
放弃.yield() 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
优先级.setPriority()线程优先级1-10,默认为5,越高获取cpu概率越高
守护线程.setDaemon(true)设置为守护线程

补充
线程有两类:

  • 用户线程(前台线程)
  • 守护线程(后台线程)

如果程序中所有的用户线程都执行完毕,守护线程也会自动结束。垃圾回收线程属于守护线程。

4. 线程安全

案例1:买票引发的安全问题

public class MyThread extends Thread{
    //static表示这个属性所有的对象共享
    static int ticket = 0;
    @Override
    public void run() {
        while (true) {
            if(ticket < 100){
                //休眠
                try {
                    Thread.sleep(500);
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                break;
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();
        MyThread my3 = new MyThread();
        //设置线程名
        my1.setName("窗口1");
        my2.setName("窗口2");
        my3.setName("窗口3");
        //开启线程
        my1.start();
        my2.start();
        my3.start();
    }
}

运行后发现有的窗口卖的票都一样或是卖多了,这样是不行的,怎么解决呢?

4.1 synchronized 同步代码块

如上案例所说,三条线程,毫无规矩的去操作票数,产生误差,现在我们拥有了synchronized同步代码块,将操作票数的代码给放进去,三条线程谁先进去先买票,卖完后出去了下一条线程才能操作买票。
格式:

synchronized () {
	操作共享数据的代码
}

特点:

  1. 锁默认是打开的,有一个线程进去了,锁自动关闭。
  2. 里面的代码全部执行完毕后,线程出来,锁自动打开。

示例:

public class MyThread extends Thread{
    //static表示这个属性所有的对象共享
    static int ticket = 0;
    //锁对象,一定要是唯一的
    static final Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if(ticket < 100){
                    //休眠
                    try {
                        Thread.sleep(200);
                        ticket++;
                        System.out.println(getName() + "正在卖第" + ticket + "张票");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    break;
                }
            }
        }
    }
}

注意:

  • synchronized同步代码块不能写在循环的外边,否则会出现一点小问题;如果写在循环外边,第一个抢到CPU执行权的线程,它要把代码走完,怎么走完,票卖完了就ok了!那么就是说,其他线程就算抢到了执行权也没用,一切都结束了。
  • synchronized后面的锁对象一定要是唯一的;假设有俩条线程,现在锁对象不是唯一的,那么两条线程看到的锁就是不一样的,这样你的同步代码块还有什么意义呢?一般我们可以把当前类的字**节码文件(当前类名.class)**当作锁对象。

4.2 同步方法

所谓的同步方法就是把synchronized关键字添加到方法上。
格式:

修饰符	synchronized	返回值类型	方法名(方法参数){...}

特点:

  1. 同步方法是锁住方法里面的所有代码。
  2. 锁对象不能自己指定,是java已经指定好的;如果当前方法是非静态的,锁对象为this,如果当前方法是静态的,锁对象为当前类的字节码文件

示例:

public class MyRunnable implements Runnable {
    int ticket = 0;

    @Override
    public void run() {
        //1.循环
        while (true) {
            //2.同步代码块(最后提取为同步方法)
            if (MyMethod()) break;
        }
    }

    private synchronized boolean MyMethod() {
        if (ticket == 100) {
            //3.判断共享数据是否到了末尾,如果到了
            return true;
        } else {
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //4.判断共享数据是否到了末尾,如果没到
            ticket++;
            System.out.println(Thread.currentThread().getName()+ "正在卖第" + ticket + "张票");
        }
        return false;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable, "窗口1");
        Thread t2 = new Thread(myRunnable, "窗口2");
        Thread t3 = new Thread(myRunnable, "窗口3");

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

上面的两种方式都可以理解为自动锁,例如同步代码块,当你执行玩之后,锁会自动打开。下面我们来了解一下手动的加锁与开锁。

4.3 lock锁

  • JDK5以后提供了一个新的锁对象Lock,它实现提供比synchronized方法和语句跟广泛的锁定操作。
  • Lock中提供了获得锁和释放锁的方法:void lock():获得锁 / void unlock():释放锁.
  • Lock是接口不能直接实例化,采用它的实现类ReentrantLock来实例化 。
    示例:
public class MyRunnable implements Runnable {
    static int ticket = 0;

    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        //循环
        while (true) {
            //手动加锁
            lock.lock();
            try {
                if (ticket == 100) {
                    //3.判断共享数据是否到了末尾,如果到了
                    break;
                } else {
                    Thread.sleep(30);
                    //4.判断共享数据是否到了末尾,如果没到
                    ticket++;
                    System.out.println(Thread.currentThread().getName()+ "正在卖第" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //解锁
                lock.unlock();
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable, "窗口1");
        Thread t2 = new Thread(myRunnable, "窗口2");
        Thread t3 = new Thread(myRunnable, "窗口3");

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

4.4 死锁

死锁?所谓死锁就是在我们程序中出现了锁的嵌套(一个锁套装一个锁)。这可不是一个知识点,这是一个错误。

示例:
一男一女,一双筷子,每人各一只筷子,当吃饭需要一双,男女双方都各自拿个一只筷子,等对方放下筷子,这就形成了僵局。

public class MyRunnable implements Runnable {
    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        //循环
        while (true) {
            if("线程A".equals(Thread.currentThread().getName())){
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,ok!");
                    }
                }
            } else if ("线程B".equals(Thread.currentThread().getName())) {
                synchronized (objB) {
                    System.out.println("线程B拿到了B锁,准备拿A锁");
                    synchronized (objA) {
                        System.out.println("线程B拿到了A锁,ok!");
                    }
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable, "线程A");
        Thread t2 = new Thread(myRunnable, "线程B");

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

运行结果:程序卡死。
在这里插入图片描述

4.5 等待唤醒机制(生产者消费者机制)

这是一个十分经典的多线程协作模式。
在这里插入图片描述
常用方法

方法名称说明
wait()当前线程等待,直到被其他线程唤醒
notify()随即唤醒单个线程
notifyAll()唤醒所有线程

示例:

/**
 * {@code @ProjectPath} : xianCheng
 * {@code @Author} : MrLiu
 * {@code @Date} : 2023/6/6 17:08
 * <
 * 生产者:厨师
 * >
 */
public class Cook extends Thread{
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //判断平台是否有食物
                    if (Desk.foodFlag == 1) {
                        //如果有,就等待
                        try {
                            Desk.lock.wait();//让当前线程和锁绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //如果没有,就制作食物
                        System.out.println("厨师制作了一碗面条");
                        //修改桌子上食物的状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者开始吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}
/**
 * {@code @ProjectPath} : xianCheng
 * {@code @Author} : MrLiu
 * {@code @Date} : 2023/6/6 17:09
 * <
 * 消费者:吃货
 * >
 */
public class Foodie extends Thread{
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if(Desk.count == 0){
                    break;
                } else {
                    //先判断平台是否有食物
                    if (Desk.foodFlag == 0) {
                        //如果没有,就等待
                        try {
                            Desk.lock.wait();//让当前线程和锁绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //把吃的总数-1
                        Desk.count--;
                        //如果有就,开吃
                        System.out.println("吃货正在吃面条,还能再吃"+ Desk.count + "碗");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();
                        //修改桌子上食物的状态
                        Desk.foodFlag = 0;
                    }

                }
            }
        }
    }
}
/**
 * {@code @ProjectPath} : xianCheng
 * {@code @Author} : MrLiu
 * {@code @Date} : 2023/6/6 17:10
 * <
 * 平台:桌子
 * 作用:控制生产者和消费者的执行
 * >
 */
public class Desk {
    //是否有食物--->0:没有     1:有
    public static int foodFlag = 0;
    //总个数
    public static int count = 10;
    //锁对象
    public static Object lock = new Object();
}
/**
 * {@code @ProjectPath} : xianCheng
 * {@code @Author} : MrLiu
 * {@code @Date} : 2023/6/6 19:09
 * <>
 */
public class Test {
    public static void main(String[] args) {
        //创建线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();
        //设置线程名
        c.setName("厨师");
        f.setName("吃货");
        //开启线程
        c.start();
        f.start();
    }
}

等待唤醒机制(阻塞队列实现方式)
在这里插入图片描述
**注意:**生产者和消费者必须使用同一个阻塞队列。

阻塞队列的继承结构

public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            String take;
            try {
                //不断从阻塞队列中获取面条
                take = queue.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("吃了一碗"+take);
        }
    }
}
public class Cook extends Thread{
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put("面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("厨师做了一碗面条");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //1.创建阻塞队列对象
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        //2.创建线程对象,并把阻塞队列传递过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);
        //3.开启线程
        c.start();
        f.start();
    }
}

5. 线程池

以前写多线程,用到的时候就创建线程,就完之后线程就消失,这样是不对的,它会浪费操作系统的资源。怎么办呢?我们准备一个容器,用来存放线程,就是线程池。
刚开始的时候,线程池里面是空的,当我们给线程池提交一个任务的时候,线程池本身就会自动的创建一个线程,我们拿个这个线程去执行任务,执行完后将线程还回线程池,下次再提交任务的时候,线程池会将之前还回去的线程给我们用。
特殊情况:如果第一个线程正在被使用,又来了第二任务,这时线程池会自动创建第二个线程,供我们使用。同时,线程池是有上限的,这个上限我们可以自己设定。

核心原理

  • 创建一个空池子。
  • 提交任务时,池子会创建新的线程对象,任务执行完毕,线程不死亡而是还给池子,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可。
  • 但如果提交任务时,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。

线程池代码实现
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

方法名称说明
public static ExcutorsService newCachedThreadPool()创建一个没有上限的线程池
public static ExcutorsService newFixedThreadPool(int nThreads)创建一个有上限的线程池

示例:

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
        ExecutorService pool2 = Executors.newFixedThreadPool(3);

        //2.提交任务
//        pool1.submit(new MyRunnable());
//        pool1.submit(new MyRunnable());

        pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());

        //3.销毁线程池(等待所有线程执行完毕后)
//        pool1.shutdown();
    }
}
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":    >---->" + i );
        }
    }
}

自定义线程池

这里给大家说一下自定义线程池的七个元素:

  • 核心线程数(不能小于0)
  • 最大线程数(最大数量>=核心线程数量)
  • 空闲时间(值)(不能小于0)
  • 空闲时间(单位)(用TimeUnit指定)
  • 任务队列(不能为null)
  • 创建线程的方式(不能为null)
  • 要执行的任务过多时的解决方案(不能为null)

在这里插入图片描述
任务拒绝策略

方法名说明
ThreadPoolExecutor.AbortPolicy默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy丢弃任务,但不抛出异常,这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy调用任务的run()方法绕过线程池直接执行

示例:

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程数(不能小于0)
                6,//最大线程数(最大数量>=核心线程数量)
                60,//空闲时间(值)(不能小于0)
                TimeUnit.SECONDS,//空闲时间(单位)(用TimeUnit指定)
                new ArrayBlockingQueue<>(3),//任务队列(不能为null)
                Executors.defaultThreadFactory(),//创建线程的方式(不能为null)
                new ThreadPoolExecutor.AbortPolicy()//要执行的任务过多时的解决方案(不能为null)
        );
    }
}

总结· 如果不断的有任务提交,会有以下三个临界点:

  • 当核心线程满时,再提交任务就要排队。
  • 当核心线程满时,队伍满时,会创建临时线程。
  • 当核心线程满时,队伍满时,临时线程满时,会触发任务拒绝策略。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值