javaDay25

本文深入探讨Java中的线程安全问题,包括线程同步的synchronized关键字、Lock锁的使用,以及死锁的概念和示例。同时,介绍了线程同步的解决方案,如使用等待唤醒机制,以及线程组、线程池的概念和实现。此外,还涵盖了实现多线程的第三种方式——Callable接口以及定时器的使用。
摘要由CSDN通过智能技术生成

一、线程安全同步问题1

1、问题一:

分析:
  共享数据:同一个学生对象Student
  生产者:SetThread给学生对象的成员变量进行赋值操作
  消费者:GetThread获取学生对象的成员变量的值
  测试者:StudentDemo创建两个线程,一个设置值线程,一个获取值线程

问题:
  按照我们分析的步骤去写代码,运行后发现,每一次的结果都是null---0,这是必然的。
  原因:设置值的线程与获取值的线程的操作的不是同一个学生对象
  如何解决呢?
    目的:让设置值的线程与获取值的线程的操作的是同一个学生对象
    解决方式:在外界创建出一个学生对象,通过参数的形式让两个线程操作同一个对象
public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();

        //创建自定义类对象
        SetThread setThread = new SetThread(s);
        GetThread getThread = new GetThread(s);

        //使用Thread类创建线程对象,将自定义对象作为参数传递
        Thread proThread = new Thread(setThread);
        Thread cusThread = new Thread(getThread);

        proThread.setPriority(1);
        cusThread.setPriority(10);

        //启动线程
        proThread.start();
        cusThread.start();
    }
}
public class SetThread implements Runnable{
    private Student s;

    SetThread(Student student){
        s = student;
    }

    @Override
    public void run() {
//        Student s = new Student();
        s.name = "王宇";
        s.age = 18;
    }
}
public class GetThread implements Runnable{
    private Student s;

    GetThread(Student student){
        s = student;
    }

    @Override
    public void run() {
//        Student s = new Student();
        System.out.println(s.name+"---"+s.age);
    }
}
public class Student {
    String name;
    int age;
}

2、问题二:

问题二:我们为了方便观察更好的结果来修改,于是我们加入了循环和判断,不同的判断给同一个学生对象赋不同的值
但是呢,我们在运行的时候出现新的问题
  1、同一条数据出现了多次
  2、姓名和年龄与代码中不匹配

原因:
  1、同一条数据出现了多次
    由于CPU小小的时间片就可以执行很多次
  2、姓名和年龄与代码中不匹配
    这是由于线程的执行具有随机性导致的

产生线程同步安全问题:
  1、是否存在多线程环境   是
  2、是否存在共享数据    是
  3、是否有很多条语句操作着共享数据    是

既然都满足,那就说明线程不安全

解决方案:
  1、关键字加锁
  2、Lock锁
注意事项:
  1、不同种类的线程对象都需要加锁
  2、不同种类的线程对象拥有的必须是同一把锁
public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        Lock lock = new ReentrantLock();

        //创建自定义类对象
        SetThread setThread = new SetThread(s, lock);
        GetThread getThread = new GetThread(s, lock);

        //使用Thread类创建线程对象,将自定义对象作为参数传递
        Thread proThread = new Thread(setThread);
        Thread cusThread = new Thread(getThread);

        proThread.setPriority(1);
        cusThread.setPriority(10);

        //启动线程
        proThread.start();
        cusThread.start();
    }
}
import java.util.concurrent.locks.Lock;

public class SetThread implements Runnable{
    private Student s;
    private Lock lock;

    private int i = 0;

    public SetThread(Student student){
        s = student;
    }

    SetThread(Student student, Lock lock){
        s = student;
        this.lock = lock;
    }

    @Override
    public void run() {
//        Student s = new Student();
//        s.name = "王宇";
//        s.age = 18;
//        while(true){
//            synchronized(s){
//                if (i % 2 == 0){
//                    s.name = "王宇";
//                    s.age = 18;
//                }else{
//                    s.name = "张保桂";
//                    s.age = 19;
//                }
//                i++;
//            }
//        }
        while (true){
            lock.lock();
            if(i % 2 == 0){
                s.name = "王宇";
                s.age = 18;
            }else{
                s.name = "张保桂";
                s.age = 19;
            }
            i++;
            lock.unlock();
        }
    }
}
import java.util.concurrent.locks.Lock;

public class GetThread implements Runnable {
    private Student s;
    private Lock lock;

    public GetThread(Student student) {
        s = student;
    }

    GetThread(Student student, Lock lock) {
        s = student;
        this.lock = lock;
    }

    @Override
    public void run() {
//      Student s = new Student();
//        while (true){
//            synchronized(s){
//                System.out.println(s.name+"---"+s.age);
//            }
//        }
        while (true) {
            lock.lock();
            System.out.println(s.name + "---" + s.age);
            lock.unlock();
        }
    }
}
public class Student {
    String name;
    int age;
}

3、问题三:

问题3:我们虽然解决了线程安全的问题,但是观察结果后发现,同一条数据连续消费了很多次,而这样的问题叫做生产者
消费者的问题,使用Java提供的等待唤醒机制去解决。

如何添加等待唤醒机制呢?
  所需要调用的方法在Object类中,Object类中有三个方法需要我们学习

  void notify()
    唤醒正在等待对象监视器的单个线程
  void notifyAll()
    唤醒正在等待对象监视器的所有线程
  void wait()
    导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法

    为什么这三个方法不定义在Thread类中呢?
      这些方法要想调用,必须通过锁对象调用,因为如果连锁对象都不一样了,就没必要等待唤醒了,直接执行逻辑代码
      而说到现在同步代码块的锁对象是任意对象,类型无法确定,所以这些方法都定义在Object类中,引Java所有的类都有一个共同父类Object
public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();


        //创建自定义类对象
        SetThread setThread = new SetThread(s);
        GetThread getThread = new GetThread(s);

        //使用Thread类创建线程对象,将自定义对象作为参数传递
        Thread proThread = new Thread(setThread);
        Thread cusThread = new Thread(getThread);


        //启动线程
        proThread.start();
        cusThread.start();
    }
}
import java.util.concurrent.locks.Lock;

public class SetThread implements Runnable{
    private Student s;
    private Lock lock;

    private int i = 0;

    SetThread(Student student){
        s = student;
    }

    SetThread(Student student,Lock lock){
        s = student;
        this.lock = lock;
    }

    @Override
    public void run() {
//        Student s = new Student();
        s.name = "王宇";
        s.age = 18;
        while (true){
            synchronized(s){
                //判断学生对象有没有值
                //flag初始的值是false,表示没有值,如果是true表示生产者已经生产了值
                //有值对于生产者来说,等待消费者消费数据
                if (s.flag){
                    try {
                        s.wait();//阻塞,等待消费者消费数据,才能继续往下走
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
                if (i % 2 == 0){
                    s.name = "王宇";
                    s.age = 18;
                }else{
                    s.name = "张保桂";
                    s.age = 19;
                }
                i++;

                //生产者生产完数据后,将flag值改为true
                //通知消费者消费数据
                s.notify();
                s.flag = true;
            }
        }
//        while (true){
//            lock.lock();
//            //先看看数据有没有被消费
//            if (i % 2 == 0){
//                s.name = "王宇";
//                s.age = 18;
//            }else{
//                s.name = "张保桂";
//                s.age = 19;
//            }
//            //通知消费者来消费数据
//            i++;
//            lock.unlock();
//        }
    }
}
import java.util.concurrent.locks.Lock;

public class GetThread implements Runnable{
    private Student s;
    private Lock lock;

    GetThread(Student student){
        s = student;
    }

    GetThread(Student student,Lock lock){
        s = student;
        this.lock = lock;
    }

    @Override
    public void run() {
//        Student s = new Student();
        while (true){
            synchronized(s){
                //判断学生对象的成员变量是否有值
                //如果flag的值是true,表示可以进行取值,反之,等待生产者生产数据
                if (!s.flag){
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name+"---"+s.age);

                //消费者消费完数据后,通知生产者生产数据
                s.notify();
                s.flag = false;
            }
        }
//        while (true){
//            lock.lock();
//            //先看看数据有没有存在
//            System.out.println(s.name+"---"+s.age);
//            //通知生产者去生产数据
//            lock.unlock();
//        }
    }
}
public class Student {
    String name;
    int age;
    boolean flag;
}

二、死锁

1、定义:

死锁:
  指的是两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象

2、举例:

中国人和美国人吃饭的案例
  假设中国人吃饭的时候,必须要有两根筷子才能吃饭
  美国人吃饭的时候,必须要有一把刀和一把叉才能吃饭

  正常情况下:
    中国人:两根筷子
    美国人:一把刀,一把叉

  发生死锁的情况:
    中国人:一根筷子,一把刀
    美国人:一根筷子,一把叉

死锁的现象更容易出现在同步嵌套情况下,所以今后开发的时候,尽量避免同步嵌套

3、代码

public class DieLockDemo {
    public static void main(String[] args) {
        //创建两个线程对象
        DieLock d1 = new DieLock(true);
        DieLock d2 = new DieLock(false);

        //启动线程
        d1.start();
        d2.start();
    }
}
public class DieLock extends Thread{
    private boolean flag;

    public DieLock(boolean flag){
        this.flag = flag;
    }
    /**
     *   现象1:
     *     if lock1
     *     else lock2
     *
     *   现象2:
     *     else lock2
     *     if lock1
     */
    @Override
    public void run() {
        if (flag){
            synchronized(MyLock.lock1){
                System.out.println("if lock1");
                synchronized(MyLock.lock2){
                    System.out.println("if lock2");
                }
            }
        }else{
            synchronized(MyLock.lock2){
                System.out.println("else lock2");
                synchronized(MyLock.lock1){
                    System.out.println("else lock1");
                }
            }
        }
    }
}

三、线程同步

1、同步安全解法一:synchronized关键字

1、线程同步的好处

解决了多线程的安全问题

2、线程同步的弊端

加了一个同代码块后,就相当于加了一把锁,每次进入同步代码块的时候都会去判断一下
无形之中,降低了我们执行效率

3、代码

public class SellTicketDemo1 {
    public static void main(String[] args) {
        TicketWindow1 ticketWindow1 = new TicketWindow1();

        //使用Thread类创建多个线程对象
        Thread window1 = new Thread(ticketWindow1);
        Thread window2 = new Thread(ticketWindow1);
        Thread window3 = new Thread(ticketWindow1);

        //给线程起名字
        window1.setName("窗口1");
        window2.setName("窗口2");
        window3.setName("窗口3");

        //启动线程
        window1.start();
        window2.start();
        window3.start();
    }
}
public class TicketWindow1 implements Runnable{
    private int tickets = 100;

    private Object object = new Object();

    @Override
    public void run() {
        while(true){
            synchronized(object){
                if (tickets>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                }
            }
        }
    }
}

2、synchronized改进

1、同步代码块上的锁对象是什么呢?
  任意对象,但是需要注意的是,多个线程之间的锁对象要一样

2、同步方法呢?
  将synchronized关键字放在方法的定义上
  同步方法的锁对象是this

3、静态的同步代码块的锁对象是谁呢?
  class文件对象,是线程类的字节码文件对象,其他的字节码文件对象不行
public class SellTicketDemo2 {
    public static void main(String[] args) {
        TicketWindow2 ticketWindow1 = new TicketWindow2();

        //使用Thread类创建多个线程对象
        Thread window1 = new Thread(ticketWindow1);
        Thread window2 = new Thread(ticketWindow1);
        Thread window3 = new Thread(ticketWindow1);

        //给线程起名字
        window1.setName("窗口1");
        window2.setName("窗口1");
        window3.setName("窗口1");

        //启动线程
        window1.start();
        window2.start();
        window3.start();

//        Hashtable<String,String> stringStringHashtable = new Hashtable<>();
    }
}
public class TicketWindow2 implements Runnable{
//    private int tickets = 100;
    private static int tickets = 100;

    private Object object = new Object();
    private Demo demo = new Demo();

    int i = 0;

    @Override
    public void run() {
//        while(true){
//            synchronized(demo){
//                if(tickets>0){
//                    try {
//                        Thread.sleep(100);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
//                }
//            }
//        }
        while(true){
            if(i % 2 == 0){
                synchronized(TicketWindow2.class){
                    if (tickets > 0){
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
                    }
                }
            }else{
                sellTickets();
            }
            i++;
        }
    }
    //同步方法
//    public synchronized void sellTickets(){
//        if (tickets>0){
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
//        }
//    }
    //静态同步方法
    public synchronized static void sellTickets(){
        if(tickets>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
        }
    }
}
class Demo{

}

3、解决同步安全问题第二种解法:加lock锁

在此之前我们解决同步线程安全的时候,都是使用synchronized关键字,通过一顿分析后,将需要被包裹的代码给包起来,但是呢我们并没有
实际看到在哪里上了锁,又或是在哪里释放了锁,让其他线程获取到锁对象,执行线程。
Lock(接口)
  具体的子类:Class ReentrantLock
    lock()加锁
    unlock()释放锁
public class SellTicketDemo3 {
    public static void main(String[] args) {
        TicketWindow3 ticketWindow1 = new TicketWindow3();

        //使用Thread类创建多个线程对象
        Thread window1 = new Thread(ticketWindow1);
        Thread window2 = new Thread(ticketWindow1);
        Thread window3 = new Thread(ticketWindow1);

        //给线程起名字
        window1.setName("窗口1");
        window2.setName("窗口2");
        window3.setName("窗口3");

        //启动线程
        window1.start();
        window2.start();
        window3.start();

//        Hashtable<String,String> stringStringHashtable = new Hashtable<>();
    }
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketWindow3 implements Runnable{
    private int tickets = 100;

    private Object object = new Object();
    //创建锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            lock.lock();
            if (tickets>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
            }
            lock.unlock();
        }
    }
}

4、线程安全的类

import java.util.*;

/*
    回忆下到目前为止,我们学习哪些线程安全的类

 */
public class ThreadDemo {
    public static void main(String[] args) {
        //StringBuffer
        StringBuffer sb = new StringBuffer();
        //Vector
        Vector<String> strings = new Vector<>();
        //Hashtable
        Hashtable<String,String> stringStringHashtable = new Hashtable<>();

        //虽然Vector这个类是线程安全的,但是今后我们也不用
        //Collections工具类将线程不安全的集合转成线程安全的
        ArrayList<String> strings1 = new ArrayList<>();
        List<String> strings2 = Collections.synchronizedList(strings1);
    }
}

5、匿名内部类的形式创建线程对象

/*
    匿名内部类的形式创建线程对象
 */
public class ThreadDemo2 {
    public static void main(String[] args) {
        //1、继承Thread类,重写run方法,start()启动线程
        new Thread("王宇"){
            @Override
            public void run() {
                for(int i = 1;i<=200;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();

        //2、实现Runnable接口,实现run方法
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i =1;i <= 200;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        },"小虎").start();
    }
}

四、线程组

1、线程组

java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制
public class ThreadGroupDemo {
    public static void main(String[] args) {
        //创建自定义对象
        MyRunnable myRunnable = new MyRunnable();

        //通过Thread类创建的多个线程对象
        Thread t1 = new Thread(myRunnable, "王宇");
        Thread t2 = new Thread(myRunnable, "张保桂");

        //默认情况下,所有的线程都属于主线程组
        //public final ThreadGroup geThreadGroup();
        ThreadGroup tg1 =t1.getThreadGroup();
        System.out.println(tg1);
        ThreadGroup tg2 = t2.getThreadGroup();
        System.out.println(tg2);

        //如何获取线程组的名字
        //public final String getName()返回此线程组的名称
        String s1 = tg1.getName();
        String s2 = tg2.getName();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1.equals(s2));

        //需求:给线程分组
        //Thread(ThreadGroup group,Runnable target,String name)
        //分配一个新的Thread对象,使其具有target作为其运行对象,具有指定的name作为其名称,属于group引用的线程组
        //创建一个新的线程组
        //ThreadGroup(String name)
        //构造一个新的线程组
        ThreadGroup tg3 = new ThreadGroup("帅哥组");

        //创建线程对象并分组
        Thread t3 = new Thread(tg3, myRunnable, "李雨阳");
        Thread t4 = new Thread(tg3, myRunnable, "小虎");
        Thread t5 = new Thread(tg3, myRunnable, "刘志成");

        //获取到目前位置所有线程的名字以及所属线程组的名字
        System.out.println(t1.getName()+"属于线程组:"+t1.getThreadGroup().getName());
        System.out.println(t2.getName()+"属于线程组:"+t2.getThreadGroup().getName());
        System.out.println(t3.getName()+"属于线程组:"+t3.getThreadGroup().getName());
        System.out.println(t4.getName()+"属于线程组:"+t4.getThreadGroup().getName());
        System.out.println(t5.getName()+"属于线程组:"+t5.getThreadGroup().getName());

        //java允许程序直接对线程组进行控制
        //需求:将一个组内的所有线程设置为守护线程
        //直接通过组名进行设置,组里面的线程都是属于守护线程
        tg3.setDaemon(true);
    }
}
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i<=200;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

五、线程池

1、线程池

程序启动一个新的线程成本是比较高的,因为它涉及到要与操作系统进行交互
而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑线程池
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
在JDk5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

2、如何使用代码实现线程池呢?

1、创建线程池对象,Executors工厂类下有很多获取线程池的静态方法
  newFixedThreadPool是其中的一种线程池
  public static ExecutorService newFixedThreadPool(int nThreads)
2、如何在线程池中放线程呢?(可以存放哪些线程呢?)
3、在线程池中的线程如何去运行呢?
4、我想结束线程的运行,可不可以手动结束呢?如果可以,怎么结束?
public class ThreadPoolDemo {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //Future<?> submit(Runnable task)
        //提交一个可运行的任务执行,并返回一个表示该任务的未来
        //提交即运行
        MyRunnable myRunnable = new MyRunnable();
        pool.submit(myRunnable);//底层封装成了一个线程对象并启动执行
        pool.submit(myRunnable);
        //提交的线程数超过线程池的数量也会执行,只不过是当有空闲线程位置的时候再去执行
        pool.submit(myRunnable);

        //我想结束线程的运行,可不可以手动结束呢?如果可以,怎么结束?
        //void shutdown()
        //启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
        pool.shutdown();

        //RejectedExecutionException
        //线程池已经被关闭了,不能再继续提交任务
        pool.submit(myRunnable);
    }
}
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i<=200;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

六、实现多线程的第三种方式

自定义类实现Callable接口,实现call方法,该线程的启动必须与线程池结合,单独无法创建线程对象启动

<T> Future<T> submit(Callable<T> task)
提交值返回任务可以执行,并返回代表任务待处理结果的Future
public class ThreadPoolDemo2 {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //创建自定义Callable对象
        MyCallable myCallable = new MyCallable();

        //提交到线程池中执行
        pool.submit(myCallable);

        //手动停止线程池
        pool.shutdown();
    }
}
import java.util.concurrent.Callable;

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        Object o = new Object();

        for(int i = 1;i <= 200;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
        return o;
    }
}

七、定时器

1、Demo1

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行
在Java中,可以通过Timer和TimerTask类来实现定义调度的功能

如何实现创建定时器呢?
  Java提供了一个类给我们使用实现定时器:Timer
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo {
    public static void main(String[] args) {
        //创建定时器对象
        //Timer
        //创建一个新的计时器
        Timer timer = new Timer();

        //调度任务执行
        //void schedule(TimerTask task,long delay)
        //在指定的延迟之后安排指定的任务执行
        timer.schedule(new MyTask(timer),3000);

//        timer.cancel();
//        timer.schedule(new MyTask(timer),3000);//IllegalStateException
    }
}
class MyTask extends TimerTask {
    private Timer timer;
    public MyTask(Timer timer){
        this.timer = timer;
    }

    @Override
    public void run() {
        System.out.println("beng!!!!!爆炸了");
        timer.cancel();
    }
}

2、Demo2

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo2 {
    public static void main(String[] args) {
        //创建定时器对象
        Timer timer = new Timer();

        //void schedule(TimerTask task,long delay,long period)
        //在指定的延迟之后开始,重新执行,固定延迟执行的指定任务
        //3秒后执行任务,并且之后每隔两秒执行一次
        timer.schedule(new MyTask2(),3000,2000);
    }
}

class MyTask2 extends TimerTask {
    @Override
    public void run() {
        try {
            FileReader fr = new FileReader("ss.txt");
            BufferedReader br = new BufferedReader(fr);
            String s = br.readLine();
            System.out.println(s);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘浩浩yyds

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

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

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

打赏作者

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

抵扣说明:

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

余额充值