线程基础、线程之间的共享和协作

一、线程基础

1、基础概念

CPU核心数和线程的关系
核心数:线程数=1:1 ;使用了超线程技术后 > 1:2

CPU时间片轮转机制
又称为RR调度,会导致上下文切换

什么是进程和线程
进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:CPU调度的最小单位,必须依赖进程而存在。

并行和并发
并行:同一时刻,可以同时处理事情的能力
并发:与单位时间相关,在单位时间内可以处理事情的能力

高并发编程的意义、好处和注意事项
好处:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
问题:
线程共享资源,存在冲突;
容易导致死锁;
启用太多的线程,就有搞垮机器的可能

2、认识Java里的线程

(一)启动一个Java的main方法

public class OnlyMain {

    public static void main(String[] args) {
    //    虚拟机线程管理的接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "]" + " " + threadInfo.getThreadName());
        }
    }
}

程序就会是多线程的,除了下面6种线程还可能会有其他的。

[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的
[5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
[4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程
[3] Finalizer // 调用对象的finalize(这个方法在gc启动,该对象被回收的时候被调用) 方法的线程
[2] Reference Handler //清除 Reference 的线程
[1] main //main 线程,用户程序入口

(二)线程创建的三种方式

public class NewThread {
    /**
     * 1、继承Thread类
     */
    private static class UseThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is extends Thread");
        }
    }

    /**
     * 2、实现Runnable接口
     */
    private static class UseRun implements Runnable {

        @Override
        public void run() {
            System.out.println("This is implements Runnable");
        }
    }

    /**
     * 3、实现Callable接口,允许有返回值
     */
    private static class UseCall implements Callable<String> {

        @Override
        public String call() throws Exception {
            System.out.println("This is implements Callable");
            return "CallResult";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        UseThread useThread = new UseThread();
        useThread.start();
        UseRun useRun = new UseRun();
        Thread t = new Thread(useRun);
        t.start();
        t.interrupt();
        UseCall useCall = new UseCall();
        FutureTask<String> stringFutureTask = new FutureTask<>(useCall);
        new Thread(stringFutureTask).start();
        System.out.println(stringFutureTask.get());

    }
}

(三)怎么样才能让Java里的线程安全停止工作

线程自然终止:自然是执行完成或者抛出异常
stop()、resume()、suspend()不建议使用,stop()会导致线程不会正确的释放资源,suspend()容易导致死锁。
Java线程是协作式的,而非抢占式的
调用一个线程的interrupt()方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置设为true,线程是否中断,由线程自身决定。
isInsterrupted()则是判定当前线程是否处于中断状态
static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。

示例

/**
*如何安全的中断线程
*/
public class EndThread {

    private static class UseThread extends Thread {

        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            while (!isInterrupted()) {
                System.out.println(threadName + " is run!");
            }
            System.out.println(threadName + "interrupt flag is " + isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseThread endThread = new UseThread("endThread");
        endThread.start();
        Thread.sleep(20);
        endThread.interrupt();
    }
}
/**
*如何安全的中断线程
*/
public class EndRun {

    private static class UseRun implements Runnable {

        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(name + " is run!");
            }
            System.out.println(name + "interrupt flag is "
                    +Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseRun useRun = new UseRun();
        Thread thread = new Thread(useRun, "endThread");
        thread.start();
        Thread.sleep(20);
        thread.interrupt();
    }
}

注:抛出InterruptedException异常的时候,要注意中断标志位

public class HasInterruptException {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss_SSS");

    private static class UseThread extends Thread {

        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            while (!isInterrupted()) {
                try {
                    System.out.println("UseThread:"+sdf.format(new Date()));
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    System.out.println(threadName+" catch interrupt flag is "
                            +isInterrupted()+ " at "
                            +(sdf.format(new Date())));
                    //方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
                    interrupt();
                    e.printStackTrace();
                }
                System.out.println(threadName);
            }
            System.out.println(threadName + " interrupt flag is " + isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread interruptException = new UseThread("HasInterruptException");
        interruptException.start();
        System.out.println("Main:" + sdf.format(new Date()));
        Thread.sleep(800);
        System.out.println("Main begin interrupt thread: " + sdf.format(new Date()));
        interruptException.interrupt();
    }
}

(四)线程的生命周期

线程的生命周期
Java线程具有五中基本状态

新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

整个的生命周期就是这几种状态的切换。

run()和start()的区别:run()是普通对象的普通方法,只有调用了start()后,Java才会将线程对象和操作系统中
实际的线程进行映射,再执行run()。
yield():让出CPU的执行权,将线程从运行状态转到就绪状态,但是下一个时间片段,该线程依然有可能被再次选中运行。

什么是守护线程(Daemon Thread)
守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。

(2) 在Daemon线程中产生的新线程也是Daemon的。

(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
(4) 守护线程中finally语句块中的内容不能保证一定执行

public class DaemonThread {

    private static class UseThread extends Thread {
        @Override
        public void run() {
            try {
                while (!isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " I am extends Thread");
                    System.out.println(Thread.currentThread().getName() + " interrupt flag is " + isInterrupted());
                }
            }finally {
                System.out.println(".......finally");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UseThread useThread = new UseThread();
        useThread.setDaemon(true);
        useThread.start();
        Thread.sleep(5);
        //useThread.interrupt();
    }
}

二、线程间的共享

synchronized内置锁

对象锁:锁的是类的对象实例。
类锁:锁的是每个类的Class对象,每隔类的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。

对象锁的粒度要比类锁的粒度要细,引起线程竞争锁的情况比类锁要少的多,所以尽量别用类锁,锁的粒度越少越好。

/**
* 两个线程锁的是两个不同的对象,则两个线程可以同时运行,没有锁的竞争
*/
public class SynClzAndInst {
    /**
     * 使用对象锁的线程
     */
    private static class InstanceSyn implements Runnable {

        private SynClzAndInst synClzAndInst;

        public InstanceSyn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance is running ... " + synClzAndInst);
            synClzAndInst.instance();
        }
    }

    /**
     * 使用对象锁的线程
     */
    private static class Instance2Syn implements Runnable{
        private SynClzAndInst synClzAndInst;

        public Instance2Syn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is running..."+synClzAndInst);
            synClzAndInst.instance2();
        }
    }

    /**
     * 锁对象
     */
    private synchronized void instance(){
        SleepUtil.second(3);
        System.out.println("synInstance is going..."+this.toString());
        SleepUtil.second(3);
        System.out.println("synInstance ended "+this.toString());
    }

    /**
     * 锁对象
     */
    private synchronized void instance2(){
        SleepUtil.second(3);
        System.out.println("synInstance2 is going..."+this.toString());
        SleepUtil.second(3);
        System.out.println("synInstance2 ended "+this.toString());
    }

    public static void main(String[] args) {
        SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
        SynClzAndInst synClzAndInst2 = new SynClzAndInst();
        Thread t2 = new Thread(new Instance2Syn(synClzAndInst2));
        t1.start();
        t2.start();
        SleepUtil.second(1);
    }
}

运行结果:TestInstance2 线程不用等TestInstance线程运行完,他们可以同时运行

TestInstance is running ... com.jason.SynClzAndInst@4e8a6c13
TestInstance2 is running...com.jason.SynClzAndInst@6d0ca4c7
synInstance is going...com.jason.SynClzAndInst@4e8a6c13
synInstance2 is going...com.jason.SynClzAndInst@6d0ca4c7
synInstance2 ended com.jason.SynClzAndInst@6d0ca4c7
synInstance ended com.jason.SynClzAndInst@4e8a6c13
/**
* 两个线程锁的是同一个的对象,则存在锁的竞争,就必须要等一个线程释放锁,另一个线程才能运行
*/
public class SynClzAndInst {
    /**
     * 使用对象锁的线程
     */
    private static class InstanceSyn implements Runnable {

        private SynClzAndInst synClzAndInst;

        public InstanceSyn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance is running ... " + synClzAndInst);
            synClzAndInst.instance();
        }
    }

    /**
     * 使用对象锁的线程
     */
    private static class Instance2Syn implements Runnable{
        private SynClzAndInst synClzAndInst;

        public Instance2Syn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is running..."+synClzAndInst);
            synClzAndInst.instance2();
        }
    }

    /**
     * 锁对象
     */
    private synchronized void instance(){
        SleepUtil.second(3);
        System.out.println("synInstance is going..."+this.toString());
        SleepUtil.second(3);
        System.out.println("synInstance ended "+this.toString());
    }

    /**
     * 锁对象
     */
    private synchronized void instance2(){
        SleepUtil.second(3);
        System.out.println("synInstance2 is going..."+this.toString());
        SleepUtil.second(3);
        System.out.println("synInstance2 ended "+this.toString());
    }

    public static void main(String[] args) {
        SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
        Thread t2 = new Thread(new Instance2Syn(synClzAndInst));
        t1.start();
        t2.start();
        SleepUtil.second(1);
    }
}

运行结果:TestInstance 线程必须等TestInstance2 线程运行完释放锁,TestInstance 线程才能运行

TestInstance2 is running...com.jason.SynClzAndInst@24824c
TestInstance is running ... com.jason.SynClzAndInst@24824c
synInstance2 is going...com.jason.SynClzAndInst@24824c
synInstance2 ended com.jason.SynClzAndInst@24824c
synInstance is going...com.jason.SynClzAndInst@24824c
synInstance ended com.jason.SynClzAndInst@24824c
/**
 * 演示对象锁和类锁
 */
public class SynClzAndInst {

    /**
     * 使用类锁的线程
     */
    private static class SynClass extends Thread {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " is running...");
            synClass();
        }
    }

    /**
     * 使用对象锁的线程
     */
    private static class InstanceSyn implements Runnable {

        private SynClzAndInst synClzAndInst;

        public InstanceSyn(SynClzAndInst synClzAndInst) {
            this.synClzAndInst = synClzAndInst;
        }
        @Override
        public void run() {
            System.out.println("TestInstance is running ... " + synClzAndInst);
            synClzAndInst.instance();
        }
    }

    /**
     * 锁对象
     */
    private synchronized void instance(){
        SleepUtil.second(3);
        System.out.println("synInstance is going..."+this.toString());
        SleepUtil.second(3);
        System.out.println("synInstance ended "+this.toString());
    }


    /**
     * 类锁,实际是锁类的class对象
     */
    private static synchronized void synClass() {
        SleepUtil.second(1);
        System.out.println("synClass going...");
        SleepUtil.second(1);
        System.out.println("synClass end...");
    }


    public static void main(String[] args) {
        SynClzAndInst synClzAndInst = new SynClzAndInst();
        Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
        t1.start();
        SynClass synClass = new SynClass();
        synClass.start();
        System.out.println("休眠开始");
        SleepUtil.second(1);
        System.out.println("休眠结束");
    }
}

运行结果:TestInstance 线程和Thread-1也能同时的运行,没有锁的竞争

休眠开始
TestInstance is running ... com.jason.SynClzAndInst@22d07f5e
Thread-1 is running...
synClass going...
休眠结束
synClass end...
synInstance is going...com.jason.SynClzAndInst@22d07f5e
synInstance ended com.jason.SynClzAndInst@22d07f5e

volatile关键字

1、保证内存可见性
在这里插入图片描述
如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。

此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

2、禁止指令重排序
指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序。比方说下面的代码:

int i = 1;
int j = 2;
上述的两条赋值语句在同一个线程之中,根据程序上的次序,“int i = 1;”的操作要先行发生于“int j = 2;”,但是“int j = 2;”的代码完全可能会被处理器先执行。JVM会保证在单线程的情况下,重排序后的执行结果会和重排序之前的结果一致。但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现,代码如下所示:

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

为instance分配内存
初始化instance
将instance变量指向分配的内存空间
由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。

2.1 内存屏障
volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:

屏障点描述
每个volatile写的前面插入一个store-store屏障禁止上面的普通写和下面的volatile写重排序
每个volatile写的后面插入一个store-load屏障禁止上面的volatile写与下面的volatile读/写重排序
每个volatile读的后面插入一个load-load屏障禁止下面的普通读和上面的volatile读重排序
每个volatile读的后面插入一个load-store屏障禁止下面的普通写和上面的volatile读重排序

上面的插入策略非常保守,但是它可以保证在任意处理器平台上的正确性。在实际执行时,编译器可以省略没必要的屏障点,同时在某些处理器上会做进一步的优化。

3、不保证原子性
需要重点说明的一点是,尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。请看以下的例子:

public class VolatileUnsafe {

    private static class VolatileVar implements Runnable {

        private volatile int a = 0;

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            a = a + 1;
            System.out.println(threadName + ":========" + a);
            SleepUtil.ms(100);
            a = a + 1;
            System.out.println(threadName + ":========" + a);
        }
    }

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

执行结果如下:

Thread-2:========2
Thread-3:========3
Thread-0:========2
Thread-1:========4
Thread-3:========5
Thread-0:========7
Thread-1:========8
Thread-2:========7

所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,代码和结果如下

public class VolatileUnsafe {

    private static class VolatileVar implements Runnable {

        private volatile int a = 0;

        @Override
        public synchronized void run() {
            String threadName = Thread.currentThread().getName();
            a = a + 1;
            System.out.println(threadName + ":========" + a);
            SleepUtil.ms(100);
            a = a + 1;
            System.out.println(threadName + ":========" + a);
        }
    }

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

结果

Thread-2:========1
Thread-2:========2
Thread-1:========3
Thread-1:========4
Thread-0:========5
Thread-0:========6
Thread-3:========7
Thread-3:========8

ThreadLocal

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
可以把他理解成为一个map,类型是Map<Thread, Integer>
示例

public class UseThreadLocal {

    /**
     * 可以理解为 一个map,类型 Map<Thread,Integer>
     */
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };

    /**
     * 运行三个线程
     */
    public void startThreadArray() {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new TestThread(i));
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    /**
     *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
     */
    public static class TestThread implements Runnable {
        int id;
        public TestThread(int id) {
            this.id = id;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ": start!!!");
            Integer integer = threadLocal.get();
            integer = integer + id;
            threadLocal.set(integer);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());

        }
    }

    public static void main(String[] args) {
        UseThreadLocal useThreadLocal = new UseThreadLocal();
        useThreadLocal.startThreadArray();
    }
}

执行结果如下:

Thread-0: start!!!
Thread-2: start!!!
Thread-0: 1
Thread-1: start!!!
Thread-2: 3
Thread-1: 2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值