Java基础—多线程(一)

多线程(一)

一、进程和线程

  1. 1.区别和联系
    1. 区别:
      进程是一个正在进行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或一个控制单元。进程是系统进行资源分配和调度的一个独立单位。每一个应用程序启动的时候,都会被分配一定的内存空间。进程是用于标识这片内存空间,用于封装内存空间里的控制单元。

    1. 线程是进程中一个独立的控制单元,控制着进程的执行。它是CPU分配和调度其资源的基本单位。线程没有办法脱离进程独立运行,必须包含在进程中运行。多线程之间共享进程拥有的资源。

    1. 联系:
      进程和线程都是系统创建的。一个进程中至少有一个线程。同一个进程中的多个线程可以并发执行,提高程序运行效率。

示例代码:

class ThreadTest {
    public static void main(String[] args) {
        for(int x = 0; x < 4000; x ++) {
            System.out.println("Hello World...");
        }
    }
}

用dos命令行编译上述代码时,会启动javac进程;运行上述代码时,会启动java进程,该进程中至少有一个线程在负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称为主线程。

小扩展
jvm启动的时候,不止一个线程,还有负责垃圾回收机制的进程。

二、多线程

  1. 1.多线程存在的意义
    多线程的出现能让程序产生同时运行的效果。

  1. 2.自定义线程
    1. Java提供了对线程这类事物的描述,封装为 Thread类。创建线程有两种方式:
      1)继承 Thread类,复写 run()方法,调用线程的 start()方法

示例代码:

//继承线程类
class TestThread extends Thread {
    //复写run()方法
    public void run() {
        System.out.println("TestThread running...");
    }
}

class Test {
    public static void main(String[] args) {
        //创建线程对象
        TestThread myThread = new TestThread();
        //线程开始执行
        myThread.start();
    }
}

start()方法启动线程,调用run()方法;修改代码,分析打印结果。

示例代码:

class TestThread extends Thread {
    public void run() {
        for(int x = 0; x < 60; x++) {
            System.out.println("...TestThread running" + x);
        }
    }
}

class Test {
    public static void main(String[] args) {
        TestThread myThread = new TestThread();
        myThread.start();

        for(int x = 0; x < 60; x++) {
            System.out.println("MainThread running..." + x);
        }
    }
}

程序运行部分结果:

MainThread running...0
...TestThread running0
...TestThread running1
MainThread running...1
MainThread running...2
MainThread running...3

运行结果每次都不同,因为多个线程都在获取CPU的执行权(资源);CPU执行哪个线程,就运行哪个线程里的代码;某一时刻,只有一个线程在运行(多核除外);CPU在做着快速切换,达到了看上去程序是同时运行的效果。这是多线程的随机性。如图:

这里写图片描述

程序开始运行,主线程启动,随后创建好的myThread线程启动,负责执行run()方法中的代码;主线程负责执行main()方法中的代码;CPU分配资源,交替执行两个线程,所以有如上运行结果。

小扩展1
复写run()方法的原因
Thread类用于描述线程,该类定义了一个功能用于存储线程要运行的代码,这个功能就是run()方法。

小扩展2
直接调用子类中的run()方法,程序的运行结果
示例代码:

class TestThread extends Thread {
    public void run() {
        for(int x = 0; x < 60; x++) {
            System.out.println("...TestThread running" + x);
        }
    }
}

class Test {
    public static void main(String[] args) {
        TestThread myThread = new TestThread();
        //直接调用run()方法,而非start()方法
        myThread.run();

        for(int x = 0; x < 60; x++) {
            System.out.println("MainThread running..." + x);
        }
    }
}

程序运行结果为:

...TestThread running0
...TestThread running1
...TestThread running2
...TestThread running3
...TestThread running4
...TestThread running5
        .
        .
        .
MainThread running...0
MainThread running...1
MainThread running...2
MainThread running...3
MainThread running...4
MainThread running...5
        .
        .
        .

如果直接调用run()方法,该程序并没有启动新的线程,而整个程序是由主线程完成执行的。myThread线程虽然被创建,但是始终没有启动。如图:

这里写图片描述

主线程执行到run()方法,就将run()方法中的代码先执行完,再回到main方法中,执行main方法中的代码;主线程独享CPU资源;直接调用run()方法的效果,相当于对象调用方法,没有多线程执行的特点。

小练习1
创建两个线程,和主线程交替运行。
示例代码:

package com.heisejiuhuche;

class TestThread extends Thread {
    private String name;

    TestThread(String name) {
        this.name = name;
    }

    //复写run()方法
    public void run() {
        for(int x = 0; x < 20; x++) {
            System.out.println(name + " TestThread running" + x);
        }
    }
}

class Test {
    public static void main(String[] args) {
        //创建两个线程对象
        TestThread tt1 = new TestThread("***");
        TestThread tt2 = new TestThread("---");
        //启动tt1和tt2线程
        tt1.start();
        tt2.start();

        for(int x = 0; x < 20; x++) {
            System.out.println("MainThread running..." + x);
        }
    }
}

程序部分运行结果为:

*** TestThread running4
--- TestThread running10
MainThread running...5
--- TestThread running11
*** TestThread running5
--- TestThread running12
MainThread running...6
--- TestThread running13

小练习2
用Thread类的getName()方法,打印线程名称
示例代码:

package com.heisejiuhuche;

class TestThread extends Thread {
    private String name;

    TestThread(String name) {
        this.name = name;
    }

    //复写run()方法
    public void run() {
        for(int x = 0; x < 20; x++) {
            //用Thread类的getName()方法,打印线程名称
            System.out.println(this.getName() + " TestThread running" + x);
        }
    }
}

class Test {
    public static void main(String[] args) {
        //创建线程对象
        TestThread tt1 = new TestThread();
        //启动tt1线程
        tt1.start();

        for(int x = 0; x < 20; x++) {
            System.out.println("MainThread running..." + x);
        }
    }
}

程序部分运行结果为:

Thread-0 TestThread running9
MainThread running...2
Thread-0 TestThread running10
MainThread running...3
Thread-0 TestThread running11

Thread-0即为tt1线程名称。每个线程有自己默认的名称,即Thread-[编号]。标准通用的获取当前线程名称的方法是使用Thread类的currentThread()方法:

Thread.currentThread().getName();

currentThread()方法为静态方法,获取当前线程对象,返回Thread类型。如果要自定义名称,使用Thread类的setName()方法或使用线程构造方法即可。

    1. 2)实现Runnable接口
      1. 用多线程实现一个简单的多窗口卖票程序。

示例代码:

package com.heisejiuhuche;

public class TicketBooth {
    public static void main(String[] args) {
        //创建四个线程对象,并启动
        TicketThread tt1 = new TicketThread();
        TicketThread tt2 = new TicketThread();
        TicketThread tt3 = new TicketThread();
        TicketThread tt4 = new TicketThread();
        tt1.start();
        tt2.start();
        tt3.start();
        tt4.start();
    }
}

class TicketThread extends Thread {
    //四个窗口共享100张票
    private static int ticket = 100;

    public void run() {
        //票大于0的时候,四个窗口同时卖票
        while(true) {
            if(ticket > 0) {
                System.out.println(Thread.currentThread().getName() 
                + "Selling ticket: " + ticket--);
            }
        }
    }
}

程序部分运行结果为:

Thread-2Selling ticket: 2
Thread-0Selling ticket: 3
Thread-3Selling ticket: 4
Thread-1Selling ticket: 1

上述程序声明了静态成员,由于静态成员声明周期过长,一般不建议使用静态属性。如果没有静态,则每个线程对象里有100张票,总共将卖出400张。要解决这个问题,就要使用线程的第二种创建方法:实现Runnable接口。

实现Runnable接口的步骤:
1> 声明一个类,实现Runnable接口
示例代码:
class Ticket implements Runnable {
}

2> 复写Runnable接口中的run()方法,将线程要运行的代码存放在run()方法中
示例代码:
class Ticket implements Runnable {

  1. public void run() {}

}

3> 创建Thread线程对象,并将Runnable接口的子类对象作为实际参数传给Thread类的构造方法
示例代码:
Ticket ticket = new Ticket();
Thread thread = new Thread(ticket);

4> 调用线程对象的start()方法启动线程,并调用Runnable接口子类中的run()方法
示例代码:
thread.start();

上述步骤中,第3步是重点。因为通过Thread thread = new Thread();创建的thread对象,运行的是Thread类中的run()方法,而这个run()方法里没有任何代码可以运行;需要运行的代码,写在了Runnable接口子类复写的run()方法中;那么,在创建线程对象时,要明确该线程启动之后要运行的代码,就应该在创建线程时,把拥有可运行代码的run()方法所属的对象(Runnable子类),传给线程对象。这是Thread类的构造方法之一,接收一个Runnable类型的对象作为参数:Thread thread = new Thread(ticket);``ticketRunnable接口的子类对象。这样,thread线程对象启动之后,就有了可以执行的代码,该代码在ticket复写父类的run()方法中。

完整代码:

package com.heisejiuhuche;

public class TicketBooth {
    public static void main(String[] args) {
        //创建TicketThread对象
        TicketThread tt = new TicketThread();
        //将tt传给Thread类,创建线程并启动
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
    }
}

class TicketThread implements Runnable {
    //多线程共享100张票
    private static int ticket = 100;

    public void run() {
        //票大于0的时候,四个窗口同时卖票
        while(true) {
            if(ticket > 0) {
                System.out.println(Thread.currentThread().getName() 
                + "Selling ticket: " + ticket--);
            }
        }
    }
}

程序运行部分结果:

Thread-3Selling ticket: 2
Thread-1Selling ticket: 3
Thread-0Selling ticket: 4
Thread-2Selling ticket: 1

无需加静态,四个线程共享100张票的数据。

    1. 3)两种创建线程方式的区别
      1. 1> 实现Runnable接口
        实现的方式避免了单继承的局限性,同时让资源独立共享(实现的方式中, 100张票是被四个线程共享)。以学生,工人和人的关系为例,学生继承了人,现在学生类当中有一部分代码,需要多线程的支持。但是Java只支持单继承,学生已经无法再继承 Thread类来获取多线程功能。那么这时, Runnable接口就可以让学生在单继承模式下,同时获得多线程的功能。学生只需实现 Runnable接口即可。开发中,建议使用实现方式创建多线程。

      1. 2> 继承 Thread类,线程代码存放于 Thread子类的 run()方法中;实现 Runnable接口,线程代码存放于 Runnable接口子类的 run()方法中

  1. 3.线程运行状态

这里写图片描述

线程有4个一般状态和1个临时状态:
1)被创建的状态;
2)运行状态;线程被创建之后,通过调用start()方法启动,进入运行状态;
3)冻结状态;冻结状态可以理解为无执行资格的状态;调用了sleep(),wait()方法的线程,会进入冻结状态;冻结状态的线程接收到notify(),即脱离冻结状态,此时如果获得执行权限,则转入运行状态;如果没有获得执行权限,则转入临时阻塞状态;
4)消亡状态;运行中的线程调用stop()方法,或run()方法结束,线程转入消亡状态;
5)临时阻塞状态;转入运行状态的线程,如果丧失CPU执行权限,则转入阻塞状态;重新获得CPU执行权限,则返回运行状态

  1. 4.多线程安全问题
    1. 1)多线程安全问题模拟

示例代码:

package com.heisejiuhuche;

public class TicketBooth {
    public static void main(String[] args) {
        TicketThread tt = new TicketThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
    }
}

class TicketThread implements Runnable {
    private static int ticket = 100;

    public void run() {
        while(true) {
            if(ticket > 0) {
                try {
                    //让当前线程睡眠10毫秒
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() 
                + "Selling ticket: " + ticket--);
            }
        }
    }
}

程序运行部分结果:

Thread-2Selling ticket: 2
Thread-0Selling ticket: 1
Thread-1Selling ticket: 0
Thread-3Selling ticket: -2
Thread-2Selling ticket: -1

上述代码打印出了0-1-2号错票,多线程的运行出现了安全问题。加上Thread.sleep(10);这行代码,为了放大效果。安全隐患产生的原因如下,当执行:

if(ticket > 0) {
    System.out.println(Thread.currentThread().getName() + "Selling ticket: " + ticket--);
}

这块代码的时候,假设此时ticket = 1;0号线程判断ticket > 0成立,但是还没等执行打印语句,CPU切换到了1号线程,1线程判断ticket > 0成立,准备执行打印语句的时候,CPU又切换到了2号线程;2号线程判断ticket > 0成立,打印:

Thread-2Selling ticket: 1

此时ticket = 0;如果这时CPU执行0号线程,则打印:

Thread-0Selling ticket: 0

此时ticket = -1;CPU再执行1号线程,打印:

Thread-1Selling ticket: -1

以此类推,线程越多,票越多,这样的代码产生的错票越多,安全问题越严重。当多个线程在操作同一共享数据时,一个线程还没有执行完,另一个线程参与执行,导致共享数据错误。解决此类问题,只需在某一时间段,仅让一个线程操作共享数据,这个过程中,其他线程不能参与执行。Java对于对线程的安全问题,提供了专业解决方式。

    1. 2)同步代码块(synchronized关键字)

格式:

synchronized(对象) {
    需要被同步的代码;
}

同步代码块示例代码:

package com.heisejiuhuche;

public class TicketBooth {
    public static void main(String[] args) {
        TicketThread tt = new TicketThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
    }
}

class TicketThread implements Runnable {
    private static int ticket = 100;
    Object obj = new Object();

    public void run() {
        while(true) {
            //同步代码块
            synchronized(obj) {
                if(ticket > 0) {
                    System.out.println(Thread.currentThread().getName() 
                    + "Selling ticket: " + ticket--);
                }
            }
        }
    }
}

程序运行部分结果:

Thread-2Selling ticket: 5
Thread-2Selling ticket: 4
Thread-2Selling ticket: 3
Thread-2Selling ticket: 2
Thread-2Selling ticket: 1

由于票的数量比较少,可能出现有线程无法抢到CPU执行权的情况。同步代码块的对象参数,相当于一把锁,持有锁的线程,可以执行同步代码块中的代码;没有锁的线程,即使获取CPU的执行权,也不能进入同步代码块进行操作。

    1. 3)同步的前提
      1. 1> 必须有两个或两个以上线程;
        2> 必须是多线程使用同一个锁;

    1. 4)同步的利弊
      1. 1> 利:
        1. 解决了多线程的安全问题

      1. 2> 弊:
        1. 每次都要判断锁的状态,较消耗系统资源

    1. 5)同步方法
      1. 同步方法是同步的另一种表现形式。同步方法只需要在函数声明时加上 synchronized关键字即可。

格式如下:

public synchronized void method() {}

小练习
银行有一个金库,有两个储户分别存300,每次存100,分存3次,用多线程实现。
示例代码:

package com.heisejiuhuche;

public class BankTest {
    public static void main(String[] args) {
        Customer c = new Customer();
        //启动线程
        new Thread(c).start();
        new Thread(c).start();
    }
}

//Customer类实现Runnable接口
class Customer implements Runnable {
    private Bank b = new Bank();
    private final int NUM = 100;

    //复写run()方法,run()方法调用Bank类的add()方法
    public void run() {
        for(int x = 0; x < 3; x++) {
            b.add(NUM);
        }
    }
}

class Bank {
    private int sum = 0;

    //add()方法每次让sum加100
    void add(int num) {
        sum += num;
        System.out.println("sum = " + sum);
    }
}

程序运行结果:

sum = 200
sum = 200
sum = 400
sum = 300
sum = 500
sum = 600

没有输出100的原因就在于,这段代码:

sum += num;
System.out.println("sum = " + sum);

出现了多线程安全问题。假设sum = 0;当前一个线程执行了sum += num;之后丧失了执行权;此时sum = 100;下一个线程抢到了执行权,执行了这两句代码,sum = 200;那么上一个线程重新获得执行权输出的时候,就不会输出sum = 100;而又输出sum = 200;

在写多线程程序时,要注意:
1> 明确哪些代码是多线程要运行的代码;(两个run()方法)
2> 明确共享数据;(Bank的b对象和sum变量)
3> 明确多线程运行代码中哪些语句是操作共享数据的(add()方法中的代码)
因此,应该修改Bank类中的add()方法中的代码为同步代码块。
示例代码:

class Bank {
    private int sum = 0;
    Object obj = new Object();
    //add()方法每次让sum加100
    void add(int num) {
        //声明同步代码块
        synchronized(obj) {
            sum += num;
            System.out.println("sum = " + sum);
        }
    }
}

做出修改之后的程序运行结果:

sum = 100
sum = 200
sum = 300
sum = 400
sum = 500
sum = 600

也可以使用同步方法:

class Bank {
    private int sum = 0;
    Object obj = new Object();
    //add()方法每次让sum加100
    public synchronized void add(int num) { //同步方法
            sum += num;
            System.out.println("sum = " + sum);
    }
}

    1. 6)同步方法——this锁
      1. 同步方法中,没有 obj对象,但是同步方法也有锁的特性。由于方法要被对象调用,那么每个方法都有一个所属对象的引用 this,所以同步方法使用的锁就是 this

验证同步方法使用的是this锁:
下面的代码设置了一个flag,让t1t2线程在同步代码块和同步方法之间交替执行:

package com.heisejiuhuche;

public class TicketBooth {
    public static void main(String[] args) {
        TicketThread tt = new TicketThread();
        Thread t1 = new Thread(tt);
        Thread t2 = new Thread(tt);
        // 启动t1线程,t1线程在同步代码块里执行
        t1.start();
        try {
            // 启动t1线程后,让主线程休眠10毫秒,为了确保t1和t2能交替执行
            Thread.sleep(10);
        } catch (Exception e) {

        }
        // 主线程让t1线程启动后,让flag为false
        tt.flag = false;
        // 启动t2线程,t2线程在同步方法里执行
        t2.start();
    }
}

class TicketThread implements Runnable {
    private static int ticket = 100;
    Object obj = new Object();
    boolean flag = true;

    public void run() {
        if (flag) {
            while (true) {
                // 同步代码块
                synchronized (obj) {
                    if (ticket > 0) {
                        try {Thread.sleep(10);} catch(Exception e) {}
                        System.out.println(Thread.currentThread().getName()
                                + "Run Selling ticket: " + ticket--);
                    }
                }
            }
        } else {
            while (true) {
                // 同步方法
                show();
            }
        }
    }

    private synchronized void show() {
        if (ticket > 0) {
            try {Thread.sleep(10);} catch(Exception e) {}
            System.out.println(Thread.currentThread().getName()
                    + "Show Selling ticket: " + ticket--);
        }
    }
}

程序运行部分结果:

Thread-0Run Selling ticket: 4
Thread-1Show Selling ticket: 3
Thread-0Run Selling ticket: 2
Thread-1Show Selling ticket: 1
Thread-0Run Selling ticket: 0

虽然同步,但是还是出现了0号票的错误;同步之后还出现多线程安全问题,一定是同步的前提没有满足。两个或两个以上的前提满足了,但是用一个锁的前提没有满足。同步代码块用的是obj锁,同步方法用的,应该是this锁(假设阶段)。那么,现在让同步代码块也使用this锁,如果程序运行没有问题,就验证了同步方法使用的是this锁。

修改代码如下:

if (flag) {
    while (true) {
        synchronized (this) {
            if (ticket > 0) {
                try {Thread.sleep(10);} catch(Exception e) {}
                System.out.println(Thread.currentThread().getName()
                        + "Run Selling ticket: " + ticket--);
            }
        }
    }
}

this代替obj传给同步代码块,程序运行结果如下:

Thread-0Run Selling ticket: 7
Thread-0Run Selling ticket: 6
Thread-1Show Selling ticket: 5
Thread-1Show Selling ticket: 4
Thread-1Show Selling ticket: 3
Thread-1Show Selling ticket: 2
Thread-1Show Selling ticket: 1

程序运行没有问题,验证了同步方法使用的是this锁。

    1. 7)静态同步方法——Class对象锁

将show()方法声明为静态,分析程序运行结果:

private static synchronized void show() {
    if (ticket > 0) {
        try {Thread.sleep(10);} catch(Exception e) {}
        System.out.println(Thread.currentThread().getName()
                + "Show Selling ticket: " + ticket--);
    }
}

此时,同步代码块中,仍旧使用this锁:

synchronized (this) {
    if (ticket > 0) {
        try {Thread.sleep(10);} catch(Exception e) {}
        System.out.println(Thread.currentThread().getName()
                + "Run Selling ticket: " + ticket--);
    }
}

程序运行结果为:

Thread-0Run Selling ticket: 2
Thread-1Show Selling ticket: 1
Thread-0Run Selling ticket: 0

程序出现多线程安全问题。如果同步方法被静态修饰,使用的锁不是this,因为静态方法中没有this对象。类加载进内存的时候,先被编译成字节码文件对象,然后静态成员加载进内存。此时,内存中没有本类对象,只有一个已被编译的类的字节码文件对象(类名.class)。由此推测,静态同步方法使用的是类名.class锁。

修改程序,将类名.class代替this传给同步代码块,如果程序运行正常,证明推论正确:

synchronized (TicketThread.class) { //同步代码块所在类为 TicketThread类
    if (ticket > 0) {
        try {Thread.sleep(10);} catch(Exception e) {}
        System.out.println(Thread.currentThread().getName()
                + "Run Selling ticket: " + ticket--);
    }
}

程序运行结果为:

Thread-1Show Selling ticket: 7
Thread-1Show Selling ticket: 6
Thread-1Show Selling ticket: 5
Thread-0Run Selling ticket: 4
Thread-0Run Selling ticket: 3
Thread-0Run Selling ticket: 2
Thread-0Run Selling ticket: 1

多次测试,程序运行正常;静态同步方法使用的锁是该方法所在类的字节码文件对象,也就是类名.class

三、线程死锁

  1. 1.定义
    1. 两个线程分别持有各自的锁,一个线程想执行另一个线程的代码块,需要取得另一个线程的锁;而另一个线程想做同样的事情;两个线程互相不放弃自己的锁,又同时需要对方的锁的现象,就是线程死锁。线程死锁会导致程序无响应。

  1. 2.线程死锁的原因
    1. 线程死锁通常出现于同步中嵌套同步,而使用的锁又不同的情况。

示例代码:

package com.heisejiuhuche;

public class DeadLockTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new DeadLock(true));
        Thread t2 = new Thread(new DeadLock(false));
        t1.start();
        t2.start();
    }
}

class DeadLock implements Runnable {
    private boolean flag;
    DeadLock(boolean flag) {
        this.flag = flag;
    }

    public void run() {
        //判断语句让一个线程在if里运行,一个在else里运行
        if(flag) {
            //同步代码块的嵌套使用不同的锁,产生死锁现象
            synchronized(Lock.lockA) {
                System.out.println("If LockA---------------");
                synchronized(Lock.lockB) {
                    System.out.println("If LockB**************");
                }
            }
        } else {
            synchronized(Lock.lockB) {
                System.out.println("IF LockB*************");
                synchronized(Lock.lockA) {
                    System.out.println("If LockA---------------");
                }
            }
        }
    }
}

//Lock类,负责产生两个不同的锁
class Lock {
    static Object lockA = new Object();
    static Object lockB = new Object();
}

程序运行结果:

IF LockB*************
If LockA---------------

打印两行之后,程序无响应,线程死锁现象产生。开发中要避免线程死锁的情况。

四、单例设计模式

前方高能(面试用):
懒汉式是实例的延时加载;
懒汉式延时加载在多线程访问时会出现安全问题,可以用双重判断加同步的方式,较高效解决安全问题;
懒汉式加同步时,使用的锁是该类的字节码文件对象,即类名.class
示例代码:
class Single {
    private static Single s = null; //延时加载
    private Single() {}

    public static Single getInstance() {
        //双重判断,提高效率
        if(s == null) {
            //同步代码块,解决多线程安全问题
            synchronized(Single.class) {
                if(s == null) {
                    s = new Single();
                }
            }
        }
        return s;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值