线程——状态、并发包、死锁、线程池、原子性

1. 进程与线程

这里首先介绍线程与进程的基本概念。

进程,操作系统中的最小单位。通过进程可以创建程序的应用。

线程,是进程中的最小单位,一个进程可能会包含多个线程,不同线程之间可能会同时执行,当不同线程同时触发时,就形成了多线程。

2.并发与并行

并发指的是同一处理器同一时间开始执行两个任务,与之相对应的是顺序,指的是处理器上一个任务完成后,才开始执行下一个任务。

并行指的是同一任务执行单元同时执行多个任务,与之相对应的是串行,指的是任务执行单元将多个任务前后排序,上一个任务完成后,才开始执行下一个任务。

总结:并行和并发是某一个线程在特定时刻以排他方式独占CPU资源,而在不同时刻,不同的线程占用CPU运行,从而实现在一段时间内同时执行多个线程的表象。

但当计算机处理器进入多核时代,严格所谓的并发与并行因为多处理器和多任务执行单元的发展而不复存在了。现在很多并行和并发更多指的是在软件层面上,进行算法和线程的优化以提升响应能力和处理能力,实现请求的快速响应和保障数据的一致性。

3.实现多线程的方式

3.1 继承Thread类

Thread类是Java当中的一个抽象类,实现多线程需要继承它,然后重写run方法。

在使用时,需要用新建一个类的对象,通过对象方法调用。

public MyThread extends Thread {
    
    //重写run方法
    public void run() {
        
   }
}

    public static void main(String[] args) {

         MyThread mythread = new MyThread();
         //启动线程
         mythread.start();

    }

3.2 实现Runnable接口

Runnable是Java中的一个接口,首先要先将线程接口实例化,再利用Thread类实现线程调用。

可以实现更高的灵活度,在实现继承的同时,还可以实现多接口和任务共享机制。

public MyRunnable implements Runnable {
    
    //重写run方法
    public void run() {
        
   }
}

 public static void main(String[] args) {

          MyRunnable myRunnable = new MyRunnable() ;
          Thread t1 = new Thread(myRunnable) ;
         //启动线程
         t1.start()
    }


   

3.3 在实现中run方法和start调用的区别

run 方法是定义在线程类中,作为自定义线程,需要重写 run
法,如果直接通过 main 主线程调用,就和调用普通的对象方法是一
样的,并不会让线程单独启动。而 start 方法是定义在 Thread 类中
的,通过 Start 方法, jvm 虚拟机会自动的调用 run 方法,执行线程任
务,所以该种方式是能够实现多线程的方式。一个线程不能够重复
执行。
4.线程调度
这里列举几个基本的线程调度方法

 4.1 setPriority

        更改线程的优先级,优先级是从1~10设置,默认是5.

Thread thread1 = new Thread(tr1,"线程优先级高") ;
 //设置线程的优先级
        thread1.setPriority(10);
        System.out.println(thread1.getPriority());
        thread1.start();
4.2  sleep
        能够让当前线程进入休眠状态,通过毫秒的参数进行设定。
public class SleepTest extends Thread {

    public void sleep(){
        while (true) {

            //每秒睡一次
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            Date date = new Date();

            System.out.println(date);
        }
    }

    public void hello() {
        for (int i = 0; i < 30; i++) {
            System.out.println(Thread.currentThread().getName() + "=" + i);
        }
    }

    @Override
    public void run()  {
//        hello();
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "=" + i);
        }
    }

    public static void main(String[] args) {
        SleepTest st = new SleepTest() ;

        //调用
        st.start();

        SleepTest st1 = new SleepTest() ;

        //调用
        st1.start();

        SleepTest st2 = new SleepTest() ;

        //调用
        st2.start();

        SleepTest st3 = new SleepTest() ;

        //调用
        st3.start();


    }

}

4.3 join

让一条线程强制插入到正在运行的线程过程中,在执行后,再让出执行权。
public void sleep(){
        while (true) {
            //每秒睡一次
            try {
                Thread.sleep(1000);
           } catch (InterruptedException e) {
                throw new RuntimeException(e);
           }
            Date date = new Date();
            System.out.println(date);
       }
   }
main:
for (int i = 0; i < 10; i++) {
          System.out.println(Thread.currentThread().getName() + "=" + i);
            if(i == 5) {
                try {
                    st.join(); //强势性,插入执行后,其他线程全部等待
               } catch (InterruptedException e){
                    throw new RuntimeException(e);
               }
           }
}

4.4 yield

线程礼让,暂停当前正在执行的线程对象,让其他线程运行,
但是不保证一定会礼让。
for (int i = 0; i < 10; i++) {
          
System.out.println(Thread.currentThread().getName() + "=" + i);
            if(i == 5) {
                Thread.yield();
}

5.线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周
期中,有几种状态呢?在 API java.lang.Thread.State 这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable (可运行)状态与非运行状态之间的转换问题。
5.1 睡眠 sleep 方法
我们看到状态中有一个状态叫做计时等待,可以通过 Thread 类的方法来进行演示 .
public static void sleep(long time) 让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行。
public class Test{
    public static void main(String[] args){
        for(int i = 1;i<=5;i++){
        Thread.sleep(1000);
        System.out.println(i)
        }
    }
}
//这时我们发现主线程执行到sleep方法会休眠1秒后再继续执行。

5.2等待和唤醒

Object 类的方法
public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用。
public class Demo1_wait {
        public static void main(String[] args) throws InterruptedException {
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行。
        new Thread(() -> {
        try {
            System.out.println("begin wait ....");
            synchronized ("") {
                    "".wait();
            }
            System.out.println("over");
            } catch (Exception e) {
        }
    }).start();
}
public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用。
public class Demo2_notify {
public static void main(String[] args) throws InterruptedException {
// 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
new Thread(() -> {
try {
System.out.println("begin wait ....");
synchronized ("") {
"".wait();
}
System.out.println("over");
} catch (Exception e) {
}
}).start();
//步骤2: 加入如下代码后, 3秒后,会执行notify方法, 唤醒wait中线程.
Thread.sleep(3000);
new Thread(() -> {
try {
synchronized ("") {
System.out.println("唤醒");
"".notify();
}
} catch (Exception e) {
}
}).start();
}
}
5.3  等待唤醒案例
定义一个集合,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。
1. 当包子没有时(包子状态为 false ),吃货线程等待 .
2. 包子铺线程生产包子(即包子状态为 true ),并通知吃货线程(解除吃货的等待状态)
生产包子类:
public class BaoZiPu extends Thread{
    private List<String> list ;
    public BaoZiPu(String name,ArrayList<String> list){
        super(name);
        this.list = list;
    }
    @Override
    public void run() {
        int i = 0;
        while(true){
//list作为锁对象
            synchronized (list){
                if(list.size()>0){
//存元素的线程进入到等待状态
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
//如果线程没进入到等待状态 说明集合中没有元素
//向集合中添加元素
                list.add("包子"+i++);
                System.out.println(list);
//集合中已经有元素了 唤醒获取元素的线程
                list.notify();
            }
        }
    }
}
}

消费包子类:

public class ChiHuo extends Thread {
    private List<String> list ;
    public ChiHuo(String name,ArrayList<String> list){
        super(name);
        this.list = list;
    }
    @Override
    public void run() {
//为了能看到效果 写个死循环
        while(true){
//由于使用的同一个集合 list作为锁对象
            synchronized (list){
//如果集合中没有元素 获取元素的线程进入到等待状态
                if(list.size()==0){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
//如果集合中有元素 则获取元素的线程获取元素(删除)
                list.remove(0);
//打印集合 集合中没有元素了
                System.out.println(list);
//集合中已经没有元素 则唤醒添加元素的线程 向集合中添加元素
                list.notify();
            }
        }
    }
}
}

测试类:

public class Demo {
public static void main(String[] args) {
//等待唤醒案例
List<String> list = new ArrayList<>();
// 创建线程对象
BaoZiPu bzp = new BaoZiPu("包子铺",list);
ChiHuo ch = new ChiHuo("吃货",list);
// 开启线程
bzp.start();
ch.start();
}
}
6.线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
6.1  线程池概念
线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

合理利用线程池能够带来三个好处:
1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多 的内存,而把服务器累趴下( 每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
6.2 线程池的使用
Java 里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一 个线程池,而只是一个执行线程的工具。真正的线程池接java.util.concurrent.ExecutorService.
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors 工程类来创建线程池对象。
Executors 类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。 ( 创建的是有界线程池, 也就是池中的线程个数可以指定最大数量) 获取到了一个线程池 ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如 下:
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行 Future 接口:用来记录线程任务执行完毕后产生的结果。
使用线程池中线程对象的步骤:
1. 创建线程池对象。
2. 创建 Runnable 接口子类对象。 (task)
3. 提交 Runnable 接口子类对象。 (take task)
4. 关闭线程池 ( 一般不做 )
Runnable 实现类代码:
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}
线程池测试类:
public class ThreadPoolDemo {
    public static void main(String[] args) {
// 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}

7.锁和死锁
7.1 lock机制
lock()锁是指是一种同步机制,用于保护共享资源的访问。它可以防止多个进程同时访问同一资源,从而避免数据竞争和死锁等问题。不同的锁适用于不同的场景,需要根据具体情况选择合适的锁来保证系统的正确性和性能。
锁的范例:
public class Lock {
    public static void main(String[] args) {
        //1.创建锁对象
        Lock lock = new ReentrantLock();
        //2.加锁 
        lock.lock();
        try {
            System.out.println("你好,现在锁上了");
        } finally {
            //3.释放锁 
            lock.unlock();
        }
    }
}

7.2死锁

死锁指的是在多线程程序中,使用了多把锁,造成线程之间相互等待,程序不往下走了。

产生死锁可能的原因有:

1.有多把锁

2.有多个线程

3.有同步代码块嵌套

产生死锁范例:
public class DeadLock implements Runnable{

    private Object obj1 = new Object() ;

    private Object obj2 = new Object() ;


    @Override
    public void run() {

        synchronized (obj1) {
            synchronized (obj2) {

            }
        }


        synchronized (obj2) {
            synchronized (obj1) {

            }
        }

    }
}

8 并发包

8.1 并发包的基本概念

JDK 的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。
4.2 并发包的实现容器 ConcurrentHashMap
public class Const {
    public static HashMap<String,String> map = new HashMap<>();
}
    public void run() {
        for (int i = 0; i < 500000; i++) {
            Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
        }
        System.out.println(this.getName() + " 结束!");
    }

测试类:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread1A a1 = new Thread1A();
        Thread1A a2 = new Thread1A();
        a1.setName("线程1-");
        a2.setName("线程2-");
        a1.start();
        a2.start();
//休息10秒,确保两个线程执行完毕
        Thread.sleep(1000 * 5);
//打印集合大小
        System.out.println("Map大小:" + Const.map.size());
    }
}
为了保证线程安全,可以使用 Hashtable 。注意:线程中加入了计时
公有、静态的集合:
public class Const {
    public static Hashtable<String,String> map = new Hashtable<>();
}
    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
        }
        long end = System.currentTimeMillis();
        System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
    }

测试类:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread1A a1 = new Thread1A();
        Thread1A a2 = new Thread1A();
        a1.setName("线程1-");
        a2.setName("线程2-");
        a1.start();
        a2.start();
//休息10秒,确保两个线程执行完毕
        Thread.sleep(1000 * 5);
//打印集合大小
        System.out.println("Map大小:" + Const.map.size());
    }
}
再看 ConcurrentHashMap
public class Const {
    public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
}
    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
        }
        long end = System.currentTimeMillis();
        System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
    }

测试类:

    public void run() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
        }
        long end = System.currentTimeMillis();
        System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
    }
结果表明,ConcurrentHashMap 仍能保证结果正确,而且提高了效率。
8 .2 CountDownLatch
CountDownLatch 允许一个或多个线程等待其他线程完成操作,再执行自己。
例如:线程 1 要执行打印: A C ,线程 2 要执行打印: B ,但线程 1 在打印 A 后,要线程 2 打印 B 之后才能打印 C,所以:线程 1 在打印 A 后,必须等待线程 2 打印完 B 之后才能继续执行。
8. .3 CyclicBarrier
CyclicBarrier 的字面意思是可循环使用( Cyclic )的屏障( Barrier )。它要做的事情是,让一组线程到达个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线 程才会继续运行。
例如:公司召集 5 名员工开会,等 5 名员工都到了,会议开始。
我们创建 5 个员工线程, 1 个开会线程,几乎同时启动,使用 CyclicBarrier 保证 5 名员工线程全部执行后,再执行开会线程。
8 .4 Semaphore
Semaphore (发信号)的主要作用是控制线程的并发数量。
synchronized 可以起到 " " 的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore 可以设置同时允许几个线程执行。
Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
8.5 Exchanger
Exchanger (交换者)是一个用于线程间协作的工具类。 Exchanger 用于进行线程间的数据交换。
这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程 也执行exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
9.原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
范例:
public class VolatileAtomicThread implements Runnable {
    // 定义一个int类型的遍历
    private int count = 0 ;
    @Override
    public void run() {
// 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            count++ ;
            System.out.println("count =========>>>> " + count);
        }
    }
}
public class VolatileAtomicThreadDemo {
    public static void main(String[] args) {
// 创建VolatileAtomicThread对象
        VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
// 开启100个线程对count进行++操作
        for(int x = 0 ; x < 100 ; x++) {
            new Thread(volatileAtomicThread).start();
        }
    }
}

9.2问题原理说明

以上问题主要是发生在 count++ 操作上:
count++ 操作包含 3 个步骤:
1 从主内存中读取数据到工作内存
2 对工作内存中的数据进行 ++ 操作
3 将工作内存中的数据写回到主内存
注意:count++ 操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。

1 )假设此时 x 的值是 100 ,线程 A 需要对改变量进行自增 1 的操作,首先它需要从主内存中读取变量 x 的值。由
CPU 的切换关系,此时 CPU 的执行权被切换到了
B 线程。 A 线程就处于就绪状态, B 线程处于运行状态
2 )线程 B 也需要从主内存中读取 x 变量的值 , 由于线程 A 没有对 x 值做任何修改因此此时 B 读取到的数据还是 100
3 )线程 B 工作内存中 x 执行了 +1 操作,但是未刷新之主内存中
4 )此时 CPU 的执行权切换到了 A 线程上,由于此时线程 B 没有将工作内存中的数据刷新到主内存,因此 A 线程
工作内存中的变量值还是 100 ,没有失效。
A 线程对工作内存中的数据进行了 +1 操作
5 )线程 B 101 写入到主内存
6 )线程 A 101 写入到主内存
虽然计算了 2 次,但是只对 A 进行了 1 次修改。
9.3 volatile 原子性测试
// 定义一个int类型的变量
private volatile int count = 0 ;
小结:在多线程环境下, volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性
(在多线程环境下 volatile 修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
9.4利用锁机制 解决问题
我们可以给 count++ 操作添加锁,那么 count++ 操作就是临界区的代码,临界区只能有一个线程去执行,所 以count++ 就变成了原子操作。
public class VolatileAtomicThread implements Runnable {
    // 定义一个int类型的变量
    private volatile int count = 0 ;
    private static final Object obj = new Object();
    @Override
    public void run() {
// 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            synchronized (obj) {
                count++ ;
                System.out.println("count =========>>>> " + count);
            }
        }
    }
}

9.5原子类

概述: java JDK1.5 开始提供了 java.util.concurrent.atomic ( 简称 Atomic ) ,这个包中的原子操作类提供 了一种用法简单,性能高效,线程安全地更新一个变量的方式。
AtomicInteger
原子型 Integer ,可以实现原子更新操作
演示基本操作:

9.6 CAS机制:

CAS 的全称是: Compare And Swap( 比较再交换 ); 是现代 CPU 广泛支持的一种对内存中的共享数据进行操作 的一种特殊指令。CAS 可以将 read-modify-check-write 转换为原子操作,这个原子操作直接由处理器保证。
CAS 机制当中使用了 3 个基本操作数:内存地址 V ,旧的预期值 A ,要修改的新值 B
举例:
1. 在内存地址 V 当中,存储着值为 10 的变量
2. 此时线程 1 想要把变量的值增加 1 。对线程 1 来说,旧的预期值 A=10 ,要修改的新值 B=11
3. 在线程 1 要提交更新之前,另一个线程 2 抢先一步,把内存地址 V 中的变量值率先更新成了 11
4. 线程 1 开始提交更新,首先进行 A 和地址 V 的实际值比较( Compare ),发现 A 不等于 V 的实际值,提交失败。
5. 线程 1 重新获取内存地址 V 的当前值,并重新计算想要修改的新值。此时对线程 1 来说, A=11 B=12 。这个重新尝试的过程被称为自旋。
6. 这一次比较幸运,没有其他线程改变地址 V 的值。线程 1 进行 Compare ,发现 A 和地址 V 的实际值是相等的。
7. 线程 1 进行 SWAP ,把地址 V 的值替换为 B ,也就是 12
9.7 总结:CAS Synchronized :乐观锁,悲观锁
CAS Synchronized 都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
Synchronized 是从悲观的角度出发(悲观锁)
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别
人想拿这个数据就会阻塞直到它拿到锁
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程 )。因此 Synchronized
我们也将其称之为 悲观锁 jdk 中的 ReentrantLock 也是一种悲观锁。性能较差!!
CAS 是从乐观的角度出发 :
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断
一下在此期间别人有没有去更新这个数据。
CAS 这种机制我们也可以将其称之为乐观锁。综合性能较好!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老大哥注释着你

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

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

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

打赏作者

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

抵扣说明:

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

余额充值