第七章、核心5:Thread和Object类中线程相关方法(wait/notify、sleep、join、yield)

1、方法概览

方法名简介
Threadsleep相关本表格的“相关”,指的是重载方法,如sleep有多个重载方法,但实际作用大同小异
.join主线程等待ThreaA执行完毕(ThreadA.join())
.yield相关放弃已经获取到的CPU资源
.currentThread获取当前执行线程的引用
.start,run相关启动线程相关
.interrupt相关中断线程
.stop(),suspend(),resuem()相关已废弃
Objectwait/notify/notifyAll相关让线程暂时休息和唤醒

2、wait,notify,notifyAll方法详解

2.1、作用、用法:阻塞阶段、唤醒阶段、遇到中断

(1)阻塞阶段

线程调用wait()方法,则该线程进入到阻塞状态,直到以下4种情况之一发生时,才会被唤醒

  • 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程
  • 另一个线程调用这个对象的notifyAll()方法且刚好被唤醒的是本线程
  • 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
  • 线程自身调用了interrupt

(2)唤醒阶段

  • notify会唤起单个在等待某对象monitor的线程,如果有多个线程在等待,则只会唤起其中随机的一个
  • notifyAll会将所有等待的线程都唤起,而唤起后具体哪个线程会获得monitor,则看操作系统的调度
  • notify必须在synchronized中调用,否则会抛出异常
java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at BlockedWaitingTimedWaiting.run(BlockedWaitingTimedWaiting.java:37)
	at java.lang.Thread.run(Thread.java:748)

(3)遇到中断

  • 假设线程执行了wait(),在此期间被中断,则会抛出interruptException,同时释放已经获取到的monitor

2.2、代码演示:4种情况

(1)普通用法

/**
 * Wait
 *
 * @author venlenter
 * @Description: 展示wait和notify的基本用法
 * 1. 研究代码执行顺序
 * 2. 证明wait释放锁
 * @since unknown, 2020-04-09
 */
public class Wait {
    public static Object object = new Object();

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "开始执行了");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");

            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println("线程" + Thread.currentThread().getName() + "调用了notify()");
            }
        }
    }
    
    public static void main(String[] args) {
        Thread thread1 = new Thread1();
        Thread thread2 = new Thread2();
        thread1.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}
//输出结果
Thread-0开始执行了
线程Thread-1调用了notify()
线程Thread-0获取到了锁

//解析
①Thread-0进入Thread1类synchronized代码块,获得锁,输出“Thread-0开始执行”
②然后Thread-0执行object.wait(),释放了锁
③Thread-1获得锁,进入Thread2类synchronized,执行object.notify(),输出“线程Thread-1调用了notify()”,同时Thread-0也被唤醒了
④Thread-0回到object.wait()的位置,执行下面的代码逻辑,输出“线程Thread-0获取到了锁”

(2)notify和notifyAll展示

/**
 * WaitNotifyAll
 *
 * @author venlenter
 * @Description: 3个线程,线程1和线程2首先被阻塞,线程3唤醒它们。notify,notifyAll
 * start先执行不代表线程先启动
 * @since unknown, 2020-04-11
 */
public class WaitNotifyAll implements Runnable{
    private static final Object resourceA = new Object();
    @Override
    public void run() {
        synchronized(resourceA) {
            System.out.println(Thread.currentThread().getName() + " get resourceA lock");
            try {
                System.out.println(Thread.currentThread().getName() + " wait to start");
                resourceA.wait();
                System.out.println(Thread.currentThread().getName() + "'s waiting end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new WaitNotifyAll();
        Thread threadA = new Thread(r);
        Thread threadB = new Thread(r);
        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    resourceA.notifyAll();
                    //resourceA.notify();
                    System.out.println("ThreadC notifyed.");
                }
            }
        });
        threadA.start();
        threadB.start();
        Thread.sleep(200);
        threadC.start();
    }
}
//输出结果
Thread-0 get resourceA lock
Thread-0 wait to start
Thread-1 get resourceA lock
Thread-1 wait to start
ThreadC notifyed.
Thread-1's waiting end
Thread-0's waiting end

(3)只释放当前monitor展示

/**
 * WaitNotifyReleaseOwnMonitor
 *
 * @author venlenter
 * @Description: 证明wait只释放当前的那把锁
 * @since unknown, 2020-04-11
 */
public class WaitNotifyReleaseOwnMonitor {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) {
        Thread thread1  = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println("ThreadA got resourceA lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadA got resourceB lock.");
                        try {
                            System.out.println("ThreadA releases resourceA lock.");
                            resourceA.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resourceA) {
                    System.out.println("ThreadB got resourceA lock.");
                    System.out.println("ThreadB tries to resourceB lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadB got resourceB lock.");
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
//输出结果
ThreadA got resourceA lock.
ThreadA got resourceB lock.
ThreadA releases resourceA lock.
ThreadB got resourceA lock.
ThreadB tries to resourceB lock.
//没有打印ThreadB got resourceB lock.(因为只调用了A.wait,只释放了lockA,B还没占用着)

2.3、特点、性质

  • 使用的时候必须先拥有monitor(synchronized锁)
  • notify只能唤醒其中一个
  • 属于Object类

2.4、原理

2.4.1 手写生产者消费者设计模式

  •  
  •  
  • 什么是生产者消费者模式
/**
 * ProducerConsumerModel
 *
 * @author venlenter
 * @Description: 用wait/notify来实现
 * @since unknown, 2020-04-11
 */
public class ProducerConsumerModel {
    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {
    private EventStorage storage;

    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

class Consumer implements Runnable {
    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

class EventStorage {
    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    public synchronized void put() {
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("仓库中已经有" + storage.size() + "个产品。");
        notify();
    }

    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
        notify();
    }
}
//输出结果
仓库中已经有1个产品。
仓库中已经有2个产品。
仓库中已经有3个产品。
仓库中已经有4个产品。
仓库中已经有5个产品。
仓库中已经有6个产品。
仓库中已经有7个产品。
仓库中已经有8个产品。
仓库中已经有9个产品。
仓库中已经有10个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下9
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下8
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下7
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下6
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下5
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下4
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下3
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下2
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下1
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0
仓库中已经有1个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0

2.5、注意点

2.6、常见面试问题

2.6.1 两个线程交替打印0~100的奇偶数

  • 基本方式:用synchronized关键字实现
/**
 * WaitNotifyPrintOddEvenSyn
 *
 * @author venlenter
 * @Description: 两个线程交替打印0~100的奇偶数,用synchronized关键字实现
 * @since unknown, 2020-04-12
 */
public class WaitNotifyPrintOddEvenSyn {
    public static int count = 0;
    public static final Object lock = new Object();

    //新建2个线程
    //1个只处理偶数,第二个只处理奇数(用位运算)
    //用synchronized来通信
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 0) {
                            System.out.println((Thread.currentThread().getName() + ":" + count++));
                        }
                    }
                }
            }
        }, "偶数").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 1) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                        }
                    }
                }
            }
        }, "奇数").start();
    }
}
//输出结果
//输出正确,但是实际上如果thread1(偶数线程)一直支持lock,会有不断循环做无效的操作
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100
  • 更好的方法:wait/notify
/**
 * WaitNotifyPrintOddEvenWait
 *
 * @author venlenter
 * @Description: 两个线程交替打印0~100的奇偶数,用wait和notify
 * @since unknown, 2020-04-12
 */
public class WaitNotifyPrintOddEvenWait {
    private static int count = 0;
    private static Object lock = new Object();
    //1. 拿到锁,我们就打印
    //2. 打印完,唤醒其他线程,自己就休眠
    static class TurningRunner implements Runnable {
        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    //拿到锁就打印
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    lock.notify();
                    if (count <= 100) {
                        try {
                            //如果任务还没结束,就让出当前线程,并休眠
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new TurningRunner(),"偶数").start();
        Thread.sleep(100);
        new Thread(new TurningRunner(),"奇数").start();
    }
}
//输出结果
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100

2.6.2 手写生产者消费者设计模式

2.6.3 为什么wait()需要在同步代码块内使用,而sleep()不需要

  • 正常逻辑是先执行wait,后续在执行notify唤醒。如果wait/notify不放同步代码块,执行wait的时候,线程切换去执行其他任务如notify,导致notify先于wait,就会导致后续切回wait的时候,一直阻塞着,无法释放,导致死锁。
  • 而sleep是针对本身的当前线程的,不影响

2.6.4 为什么线程通信的方法wait(),notify()和notifyAll被定义在Object类里?而sleep定义在Thread类里?

  • wait、notify、notifyAll是锁级别的操作,属于Object对象的,而线程实际上是可以持有多把锁的,如果把wait定义到Thread里面,就无法做到这么灵活的控制了

2.6.5 wait方法是属于Object对象的,那调用Thread.wait会怎么样?

  • Thread线程退出的时候,会自动调用notify,这可能不是我们所期望的,所以最好不要用Thread.wait

2.6.6 如何选择notify还是notifyAll?

  • 参考2.2、代码演示(2)notify和notifyAll展示
  • notify是唤起一个线程,选择哪个是随机的。而notifyAll是唤起所有线程,然后这些线程再次抢去夺锁

2.6.7 notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

  • 实质就跟初始状态一样,多个线程抢夺锁,抢不到的线程就等待,等待上一个线程释放锁

2.6.8 用suspend()和resume()来阻塞线程可以吗?为什么?

  • 这2个方法由于不安全,已经被弃用了。最好还是使用wait和notify

3、sleep方法详解

3.1 作用:我只想让线程在预期的时间执行,其他时候不要占用CPU资源

3.2 不释放锁

  • 包括synchronized和lock
/**
 * SleepDontReleaseMonitor
 *
 * @author venlenter
 * @Description: 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
 * @since unknown, 2020-04-15
 */
public class SleepDontReleaseMonitor implements Runnable{
    public static void main(String[] args) {
        SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
        new Thread(sleepDontReleaseMonitor).start();
        new Thread(sleepDontReleaseMonitor).start();
    }
    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() {
        System.out.println("线程" + Thread.currentThread().getName() + "获取到了monitor");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块");
    }
}
//输出结果
线程Thread-0获取到了monitor
线程Thread-0退出了同步代码块(5s后出现)
线程Thread-1获取到了monitor
线程Thread-1退出了同步代码块(5s后出现)
/**
 * SleepDontReleaseLock
 *
 * @author venlenter
 * @Description: 演示sleep不释放lock(lock需要手动释放)
 * @since unknown, 2020-04-15
 */
public class SleepDontReleaseLock implements Runnable {
    private static final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println("线程" + Thread.currentThread().getName() + "获取到了lock");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "释放了lock");
    }

    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }
}
//输出结果
线程Thread-0获取到了lock
线程Thread-0释放了lock(5s后)
线程Thread-1获取到了lock
线程Thread-1释放了lock(5s后)
  • 和wait不同

3.3 sleep方法响应中断

  • 抛出InterruptedException
  • 清除中断状态
/**
 * SleepInterrupted
 *
 * @author venlenter
 * @Description: 每隔1s输出当前时间,被中断,观察
 * Thread.sleep()
 * TimeUnit.SECONDS.sleep()
 * @since unknown, 2020-04-15
 */
public class SleepInterrupted implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(new Date());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("我被中断了");
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepInterrupted());
        thread.start();
        Thread.sleep(6500);
        thread.interrupt();
    }
}
//输出结果
Wed Apr 15 23:09:55 CST 2020
Wed Apr 15 23:09:56 CST 2020
Wed Apr 15 23:09:57 CST 2020
Wed Apr 15 23:09:58 CST 2020
Wed Apr 15 23:09:59 CST 2020
Wed Apr 15 23:10:00 CST 2020
Wed Apr 15 23:10:01 CST 2020
我被中断了
java.lang.InterruptedException: sleep interrupted
Wed Apr 15 23:10:01 CST 2020
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.threadobjectclasscommonmethods.SleepInterrupted.run(SleepInterrupted.java:21)
	at java.lang.Thread.run(Thread.java:748)
Wed Apr 15 23:10:02 CST 2020
Wed Apr 15 23:10:03 CST 2020

3.4 sleep总结

  • sleep方法可以让线程进入Waiting状态,并且不占用CPU资源
  • 但是不释放锁,直到规定时间后再执行
  • 休眠期间如果被中断,会抛出异常并清除中断状态

3.5 sleep常见面试问题

wait/notify、sleep异同(方法属于哪个对象?线程状态怎么切换?)

(1)相同

  • 都会阻塞
  • 都可以响应中断
外层执行thread.interrupt()
try {
    wait();
    Thread.sleep();
} catch (InterruptedException e) {
    e.printStackTrace();
}

(2)不同

  • wait/notify需要在synchronized方法中,而sleep不需要
  • 释放锁:wait会释放锁,而sleep不释放锁
  • 指定时间:sleep必须传参时间,而wait有多个构造方法,不传时间则直到自己被唤醒
  • 所属类:wait/notify是Object方法,sleep是Thread类的方法

4、join方法

4.1 作用:因为新的线程加入了“我们”,所以“我们”要等他执行完再出发

4.2 用法:(在main方法中thread1.join)main等待thread1执行完毕,注意谁等谁(父等待子)

4.3 三个例子

  • 普通用法
/**
 * Join
 *
 * @author venlenter
 * @Description: 演示join,注意语句输出顺序,会变化
 * @since unknown, 2020-04-15
 */
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行完毕");
            }
        });
        thread1.start();
        thread2.start();
        System.out.println("开始等待子线程运行完毕");
        thread1.join();
        thread2.join();
        System.out.println("所有子线程执行完毕");
    }
}
//输出结果
开始等待子线程运行完毕
Thread-0执行完毕
Thread-1执行完毕
所有子线程执行完毕
  • 遇到中断
/**
 * JoinInterrupt
 *
 * @author venlenter
 * @Description: 演示join期间被中断的效果
 * @since unknown, 2020-04-21
 */
public class JoinInterrupt {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mainThread.interrupt();
                    Thread.sleep(5000);
                    System.out.println("Thread1 finished.");
                } catch (InterruptedException e) {
                    System.out.println("子线程中断");
                }
            }
        });
        thread1.start();
        System.out.println("等待子线程运行完毕");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "主线程中断了");
            thread1.interrupt();
        }
        System.out.println("子线程已运行完毕");
    }
}
//输出结果
等待子线程运行完毕
main主线程中断了
子线程已运行完毕
子线程中断
  • 在join期间,线程到底是什么状态?:Waiting
/**
 * JoinThreadState
 *
 * @author venlenter
 * @Description: 先join再mainThread.getState()
 * 通过debugger看线程join前后状态的对比
 * @since unknown, 2020-04-22
 */
public class JoinThreadState {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(mainThread.getState());
                    System.out.println("Thread-0运行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        System.out.println("等待子线程运行完毕");
        thread.join();
        System.out.println("子线程运行完毕");
    }
}
//输出结果
等待子线程运行完毕
WAITING
Thread-0运行结束
子线程运行完毕

4.4 可以使用封装工具类:CountDownLatch或CyclicBarrier

4.5 join原理

  • 源码
(1)thread.join();
(2)
public final void join() throws InterruptedException {
        join(0);
    }
(3)
 public final synchronized void join(long millis)
    throws InterruptedException {
        ...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }
  • 分析:线程在run执行完成后,JVM底层会自动调用一个notifyAll唤醒,所以即使在join()内没有notify显示调用,执行完run()后,也会唤醒
  • 等价
//        thread.join();   等价于下面synchronized的代码
        synchronized (thread) {
            thread.wait();
        }

4.6 常见面试问题

  • 在join期间,线程处于哪种线程状态?Waiting

5、yield方法

  • 作用:释放我的CPU时间片。线程状态仍然是RUNNABLE,不释放锁,也不阻塞
  • 定位:JVM不保证遵循yield逻辑
  • yield和sleep区别:yield随时可能再次被调度

6、获取当前执行线程的引用:Thread.currentThread()方法

  • 同一个方法,不同线程会打印出各自线程的名称
/**
 * CurrentThread
 *
 * @author venlenter
 * @Description: 演示打印majn, Thread-0, Thread-1
 * @since unknown, 2020-04-22
 */
public class CurrentThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        new CurrentThread().run();
        new Thread(new CurrentThread()).start();
        new Thread(new CurrentThread()).start();
    }
}
//输出
main
Thread-0
Thread-1

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Venlenter

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

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

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

打赏作者

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

抵扣说明:

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

余额充值