阶段二-Day16-JUC相关知识一

一、JUC介绍

1. 基本概念

从Java 5开始,在JDK中多出了java.util.concurrent包(简称:JUC)。

JUC主要是让开发者在多线程编程中更加简单、方便一些。

通过JDK内置了一些类、接口、关键字,补充完善了JDK对于并发编程支持的“短板”。

2. JUC中的类和接口

之前在多线程中学习的Callable和Future属于JUC的内容

通过IDEA可以看java.util.concurrent包中所有的类和接口

  1. 找到java.util.concurrent包

    1. 选中concurrent包, 右键

    2. 选择Diagrams

  2. 选择Show Diagram

3. 主要包含功能

  1. Executor:线程池

  2. Atomic:原子操作类

  3. Lock:锁

  4. Tools:信号量工具类

  5. 并发集合:提供了线程安全的集合类。

二、线程池

1. 为什么使用线程池

线程的创建和线程的销毁都会消耗一定的系统资源。如果需要频繁的新建线程、销毁线程,对于系统可能需要消耗大量资源。

如何在需要频繁新建、销毁线程的场景下保证系统响应时间更快,消耗资源更少?

答:使用线程池。

2. 什么是线程池

内存中的一块空间。这块空间里面存放一些已经实例化好的线程对象。

当代码中需要使用线程时直接从线程池获取。当代码中线程执行结束或需要销毁时,把线程重新放入回到线程池,而不是让线程处于死亡状态。

3. 使用线程池的特点

3.1 优点
  1. 降低系统资源消耗。通过重用线程对象,降低因为新建和销毁线程产生的系统消耗。

  2. 提高系统响应速度。直接从内存中获取线程对象,比新建线程更快。

  3. 提供线程可管理性。通过对线程池中线程数量的限制,避免无限创建线程导致的内存溢出或CPU资源耗尽等问题。

3.2 缺点

默认情况下,无论是否需要使用线程对象,线程池中都有一些线程对象,也就是说会占用一定内存。

三、JUC中的线程池

1. Executor介绍

Executor 线程池顶级接口, 接口中只有一个execute()方法,方法参数为Runnable类型。

​x public interface Executor {​  void execute(Runnable command);}

2. Executor关系图

3. ThreadPoolExecutor

3.1 介绍

ThreadPoolExecutor是JUC中提供的默认线程池实现类, Executor的子类。

3.2 构造方法

提供了四种参数列表的构造方法

其中包含7个参数的构造方法, 这是ThreadPoolExecutor支持的所有参数

3.3 参数详解
3.3.1 corePoolSize核心线程数

创建线程池后,默认线程池中并没有任何的线程,执行了从线程池获取线程执行任务的时候才会创建核心线程完成任务的执行。 如果没有达到指定的corePoolSize, 即使有空闲的核心线程, 也会创建新的核心线程执行任务, 直到达到了corePoolSize。 达到corePoolSize后, 从线程池获取线程执行任务, 有空闲的核心线程, 空闲的线程会执行新任务。

理解: 线程池就相当于一个公司, 核心线程数就相当于正式工人数, 新公司成立, 接一个新任务就会招聘一个正式工, 即使有空闲的正式工, 来了新任务也会招聘新的正式工来完成新任务, 直到招满为止. 招满后, 新任务就由空闲的正式工来完成。

3.3.2 workQueue阻塞队列

阻塞队列:

队列: 底层实现 数组 | 链表. 先进先出.

阻塞: 1. 队列为空时, 阻塞获取任务

2. 队列放满时, 阻塞添加任务

  1. 当线程池中的线程数目达到指定的corePoolSize后,并且所有的核心线程都在使用中, 再来获取线程执行任务, 会将任务添加到缓存任务的阻塞队列中,也就是workQueue。

  2. 队列可以设置queueCapacity 参数,表示任务队列最多能存储多少个任务。

    理解: 正式工已经招满, 此时所有的正式工都在忙着工作, 再来的新任务, 就只能排队等待。

3.3.3 maximumPoolSize最大线程数
  1. 所有的核心线程都被使用中,且任务队列已满时,线程池会创建新的线程执行任务,直到线程池中的线程数量达到maximumPoolSize。

  2. 被使用的线程数等于maximumPoolSize ,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

理解: 正式工已经招满, 此时所有的正式工都在忙着工作, 且队列中的新任务已达到队列上线, 需要招聘临时工, 来完成任务. 正式工和临时工的总数为公司的最大员工数, 如果所有的临时工也都有任务, 再来新任务公司予以拒绝。

3.3.4 keepAliveTime线程最大空闲时间
  1. 线程池中存在空闲的线程, 就会处于空闲(alive)状态, 只要超过keepAliveTime, 空闲的线程就会被销毁,直到线程池中的线程数等于corePoolSize。

  2. 如果设置了allowCoreThreadTimeOut=true(默认false),核心线程也可以被销毁。

理解: 到了淡季, 公司不忙了, 很多的正式工和临时工都空闲了, 临时工就直接解雇了, 而正式工没有重大违纪的不会解雇。

3.3.5 unitkeepAliveTime 时间单位

TimeUnit是枚举类型

public enum TimeUnit {
  //纳秒
  NANOSECONDS(TimeUnit.NANO_SCALE),
  //微妙
  MICROSECONDS(TimeUnit.MICRO_SCALE),
  //毫秒
  MILLISECONDS(TimeUnit.MILLI_SCALE),
  //秒
  SECONDS(TimeUnit.SECOND_SCALE),
  //分钟
  MINUTES(TimeUnit.MINUTE_SCALE),
  //小时
  HOURS(TimeUnit.HOUR_SCALE),
  //天
  DAYS(TimeUnit.DAY_SCALE);

  private static final long NANO_SCALE   = 1L;
  private static final long MICRO_SCALE  = 1000L * NANO_SCALE;
  private static final long MILLI_SCALE  = 1000L * MICRO_SCALE;
  private static final long SECOND_SCALE = 1000L * MILLI_SCALE;
  private static final long MINUTE_SCALE = 60L * SECOND_SCALE;
  private static final long HOUR_SCALE   = 60L * MINUTE_SCALE;
  private static final long DAY_SCALE    = 24L * HOUR_SCALE;
}

注:

1秒 = 1000毫秒 1毫秒 = 1000微秒 1微妙 = 1000纳秒

3.3.6 threadFactory线程工厂

创建线程对象

3.3.7 handler线程池拒绝策略(面试题:线程池拒绝策略有哪些)

只有当任务队列已满,且线程数量已经达到maximunPoolSize才会触发拒绝策略。

  1. AbortPolicy: 丢弃新任务,抛出异常,提示线程池已满(默认)。

  2. DisCardPolicy: 丢弃任务,不抛出异常。

  3. DisCardOldSetPolicy: 将消息队列中最先进入队列的任务替换为当前新进来的任务。

  4. CallerRunsPolicy: 由调用该任务的线程处理, 线程池不参与, 只要线程池未关闭,该任务一直在调用者线程中。

3.4 线程池总结(常见面试题)

3.4.1 参数总结:
  1. corePoolSize: 核心线程数大小

  2. maximumPoolSize:最大线程数大小(线程池中一共存在多少个线程)

  3. queueCapacity:任务队列最大长度

  4. keepAliveSize:线程最大空闲时间

  5. allowCoreThreadTimeOut:核心线程超时是否被销毁

  6. handler:拒绝策略

  7. workQueue:任务队列类型

3.4.2 创建线程总结:
  1. 当线程数小于核心线程数时,来一个任务创建一个核心线程, 直到达到指定的核心线程数。

  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

  3. 当线程数大于等于核心线程数,且任务队列已满 。

i. 若线程数小于最大线程数,创建线程, 直到达到最大线程数 。

ii. 若线程数等于最大线程数,抛出异常,拒绝任务。

4. Executors

Executors可以帮助快速实例化特定类型的线程池对象, Executors属于一个工具类, 返回值都是ExecutorService接口的实现类, 底层都是调用ThreadPoolExecutor()

4.1 SingleThreadExecutor()

  1.  效果总结:

    1. 它只会创建一条工作线程处理任务;

    2. 采用的阻塞队列为LinkedBlockingQueue, 底层为链表

4.2 newFixedThreadPool(int)

  1. 效果总结:

    1. 它是一种固定大小的线程池;

    2. corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;

    3. keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即销毁;但这里keepAliveTime无效;

    4. 阻塞队列采用了LinkedBlockingQueue, 底层为链表;

    5. 实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。

4.3 newCachedThreadPool()

  1. 效果总结:

    1. 它是一个可以无限扩大的线程池;

    2. 它比较适合处理执行时间比较小的任务;

    3. corePoolSize为0,maximumPoolSize为 Integer的最大值,意味着线程数量可以 Integer的最大值;

    4. keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;

    5. 采用SynchronousQueue(同步队列)装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。

4.4 newScheduledThreadPool()

  1. 效果总结:

    1. 它采用DelayQueue存储等待的任务

    2. 它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;

    3. DelayQueue队列:

      底层使用数组实现, 初始容量为16, 超过16个任务, 数组扩容, 每次扩容为之前的1.5倍

    4. 工作线程会从DelayQueue取已经到期的任务去执行;

    5. 执行后也可以将任务重新定时, 放入队列中;

    6. 支持定时, 周期性执行。

4.5 newWorkStealingPool()

该方法是Java 8 新增的一个方法。底层使用的是ForkJoinPool类

4.5.1 ForkJoinPool()

ForkJoinPool是Java7新增的线程池类型。是ThreadPoolExecutor的兄弟类

工作原理(工作窃取算法):把一个Thread 分叉(fork)成多个子线程。让多个子线程执行本来一个线程应该执行的任务。最后把多个线程执行结果合并

如果在分叉后一个线程执行完成,另外的线程还没有结束,会从双端队列中尾部处理任务,另一个线程从头部取任务,防止出现线程竞争。

4.6 线程池代码演示
4.6.1 固定线程数线程池
public class ThreadPool1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建固定为10个核心线程(最大也是10个线程)的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        System.out.println(Thread.currentThread().getName());

        //让线程池中的线程执行任务
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        
        //让线程池中的线程执行任务, 获取返回值 基于参数为Callable
        Future<Object> submit = executorService.submit(new Callable<Object>() {

            @Override
            public Object call() throws Exception {
                return null;
            }
        });
        //get()方法获取返回值
        System.out.println(submit.get());

        //让线程池中的线程执行任务, 获取返回值 基于参数为Runnable
        FutureTask<Integer> integerFutureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });
        //线程池执行该任务
        executorService.submit(integerFutureTask);
        System.out.println(integerFutureTask.get());
    }
}
4.6.2 延时执行线程池
public class Test {
    public static void main(String[] args) {
        //使用Executors工具类创建线程池
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);

        //演示3秒后执行一次
        ses.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3秒后");
            }
        },3 , TimeUnit.SECONDS);

        //延迟1秒后, 每隔3秒执行一个任务
        ses.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("周期执行");
            }
        }, 1, 3, TimeUnit.SECONDS);
    }
}

四、JUC中的AtomicInteger原子类

1. 原子性介绍

原子性:操作过程中不允许其他线程干扰, 可以理解为数据操作是整体,整体只有成功或失败,不允许出现部分成功部分失败

只要具备了原子性,就一定是线程安全的。

2. JUC中原子类

3. AtomicInteger

使用AtomicInteger类保证线程安全

3.1 原始方式: 使用synchronized同步锁解决
public class Test1 {
  static int a = 0;
  public static void main(String[] args) {
    //创建线程池
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    //创建五个任务
    for (int i = 0; i < 5; i++) {
      //五个线程处理任务
      executorService.submit(new Runnable() {
        @Override
        public void run() {
          //保证数据的安全性, 使用同步锁
          synchronized ("锁"){
            for (int i1 = 0; i1 < 10000; i1++) {
              a++;
            }
          }
        }
      });
    }
    try {
      Thread.sleep(2000);
      System.out.println(a);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
3.2 原子类方式: 使用AtomicInteger原子类
public class Test {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //创建五个任务
        for (int i = 0; i < 5; i++) {
            //五个线程处理任务
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int i1 = 0; i1 < 10000; i1++) {
                        //先获取再+1
                        int i2 = atomicInteger.getAndIncrement();//相当于i++
                        System.out.println(Thread.currentThread().getName() + ": " + i2);
                    }
                }
            });
            try {
                Thread.sleep(2000);
                //atomicInteger.get()获取atomicInteger的值
                System.out.println("总和:"+atomicInteger.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
3.3 原子类AtomicInteger方法介绍
public class Test3 {
  public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(0);

    //先获取atomicInteger中的值, 再将值+1   num++
    int i = atomicInteger.getAndIncrement();
    System.out.println(i); //0

    //先将值+1, 先获取atomicInteger中的值 ++num
    int i2 = atomicInteger.incrementAndGet();
    System.out.println(i2); //2

    //先获取atomicInteger中的值, 再将值-1   num--
    int i3 = atomicInteger.getAndDecrement();
    System.out.println(i3); //2

    //先将值-1, 先获取atomicInteger中的值 --num
    int i4 = atomicInteger.decrementAndGet();
    System.out.println(i4); //0

    //先获取atomicInteger中的值, 再+指定的值
    int i5 = atomicInteger.getAndAdd(10);
    System.out.println(i5); //0

    先+指定的值, 先获取atomicInteger中的值
    int i6 = atomicInteger.addAndGet(20);
    System.out.println(i6); //30

    int i7 = atomicInteger.getAndAdd(-5);
    System.out.println(i7); //30

    int i8 = atomicInteger.addAndGet(-10);
    System.out.println(i8); //15

    //改变atomicInteger中的值
    atomicInteger.set(100);

    int andSet = atomicInteger.getAndSet(200);
    System.out.println(andSet); //100

    //先获取atomicInteger中的值
    int i1 = atomicInteger.get();
    System.out.println(i1); //200
  }
}
3.4 原子类AtomicInteger底层实现

原子类AtomicInteger底层使用的是volatile和cas

4. Volatile(面试题)

在多线程环境下,volatile 关键字可以保证共享数据的可见性,有序性, 但是并不能保证对数据操作的原子性(数据操作是整体,整体只有成功或失败,不允许出现部分成功部分失败)。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的

4.1 可见性
  • 线程操作变量后, 立刻将新的变量值同步回到主内存中.

  • 线程使用变量, 会从主内存中拷贝最新的变量值, 线程正在使用变量, 无法拷贝最近变量值.

4.1.1 概念

内存可见性是指当一个线程修改了某个变量的值,其它线程总是能立即知道这个变量的变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程在B 在使用 V 值之前,能立即读到 V 的最新值(排除B正在用使用变量V)。

4.1.2 原理

现在CPU很少有单核CPU,都是双核、双核四线程、四核、四核八线程甚至更好的CPU。 CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,一定没有直接操作自己的高速缓存效率高,所以,为了提高处理速度,CPU 不直接和主内存进行通信,而是在 CPU 与内存之间加入高速缓存,它们比直接操作主内存的速度高的多。

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从主内存拷贝到高速缓存中,CPU 直接操作的是高速缓存中的数据。但在多处理器下,将可能导致各自的高速缓存数据不一致(这可能是由于可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而总线嗅探机制是实现缓存一致性的常见机制。

嗅探机制工作原理

每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

4.2 有序性
4.2.1 指令重排

为了提高性能,编译器和处理器常常会对指令做重排序。

一般重排序可以分为如下三种类型:

1. 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2. 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。

3. 内存系统重排序。由于处理器使用高速缓存,这使得加载和存储操作看上去可能是在乱序执行。

被编译器和处理器考虑

从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:

4.2.2 有序性原理

使用 volatile 修饰变量时,Java 编译器在生成字节码时,会在指令序列中插入内存屏障(CPU处理器的指令)来禁止CPU处理器重排序。

5. CAS算法(面试题)

5.1 CAS算法介绍

CAS( Compare And Swap)算法,比较和交换算法。

CAS不需要和synchronized一样让对象具有独立性、互斥性保持线程安全。而是一种无锁也可以保证线程安全的算法。

volatile + cas: 可见性 有序性 原子性, 多线程同时操作, 同一时刻多个线程同时操作, 只能一个线程能成功, 其它线程重复执行.

synchronized: 可见性 有序性 原子性, 多线程同时操作, 同一时刻只能一个线程操作, 其它线程等待.

理解:

给定代码, num++, 多个线程同时执行这段代码

1. synchronized: 一次只能有一个线程操作。

2. CAS算法: 实现思路

线程开启时候,每个线程在主内存中拷贝一个变量副本, 每个线程操作自己的副本。

1. 首先获取到num在主内存中值, 拷贝到工作内存中, 存储为一个副本。

2. 然后在工作内存中, 对num的值+1。

3. 修改主内存中num值时,会比较主内存中的num值是否和副本中值相同。

a. 如果主内存中num值和副本值相同,把主内存num值更新为新的值。

b. 如果内存中num值和副本值不同,会从第1步重新开始执行。

5.2 优点
  1. 保证数据操作的原子性,保证了线程是安全的。

  2. 这个算法相对synchronized是比较“乐观的”,它不会像synchronized一样,当一个线程访问共享数据的时候,别的线程都在阻塞。synchronized不管是否有线程冲突都会进行加锁。由于CAS是非阻塞的,它死锁问题天生免疫,并且线程间的相互影响也非常小,更重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以它要比锁的方式拥有更优越的性能。

5.3 缺点
  1. 多线程操作时每次修改值时,可能多次出现内存值和副本值不同的情况,需要循环执行多次

  2. 可能出现ABA问题

5.4 ABA问题

所谓ABA问题,其实用最通俗易懂的话语来总结就是狸猫换太子

比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题

比如有两个线程A、B:

1. 主内存中变量为 V=3。

2. 两个线程A, B, 都拷贝了主内存中的变量 E=3 。

3. A线程执行到N=E+0, 即N=3 此时A线程挂起。

4. B线程修改原值为N=E+1,即N=4。

5. 然后B觉得修改错了,然后再重新把值修改为3;

6. A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存V的值等于线程A中E的值, 都为3,(但是却不知道B曾经修改过), A线程修改成功。

7. 尽管线程A的CAS操作成功,结果也没有问题, 但不代表就没有问题, V的值是发生过改变的。

a) 有的需求,只注重结果,结果一致就可以, 可以不用考虑ABA的问题。

b) 但是有的需求,需要注重过程,必须考虑ABA的问题。

5.5 解决ABA问题

为了解决ABA问题,一般都会在操作时设置一个int类型版本号(version),每次对内存中变量操作后都让版本号加1。当需要修改变量时,除了比较副本中值和内存值以外,还需要比较版本号是否相同。JDK中AtomicStampedReference就是这种方式,提供了全局int属性

6. 源码分析AtomicInteger原子类

6.1 底层实现

原子类AtomicInteger底层使用的是volatile和cas

1. volatitle保证了可见性, 有序性, cas保证了原子性

2. CAS算法, 它包含3个参数CAS(V,E,N)。V表示主内存中的变量,E表示预期值(从主内存中拷贝的值),N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作虽然没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

6.2 源码重点
public class AtomicInteger extends Number implements java.io.Serializable {
  //直接操作内存的类
  private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
  //获取属性偏移量, 在内存中的地址
  private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
  //存储值
  private volatile int value;
  //有参构造方法
  public AtomicInteger(int initialValue) {
    value = initialValue;
  }
  //无参构造方法
  public AtomicInteger() {
  }
  //获取值
  public final int get() {
    return value;
  }
  //设置值
  public final void set(int newValue) {
    value = newValue;
  }
  //比较交换
  public final boolean compareAndSet(int expectedValue, int newValue) {
    //参数1: 当前对象 参数2: 属性对象在内存中的地址 参数3: 预期值 参数四: 新值
    return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
  }
  //先获取, 再+1 相当于a++
  public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
  }
	//先+1, 再获取 相当于++a
  public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
  }

  //调用的Unsafe中的方法
  public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
      //内存中最新的值
      v = getIntVolatile(o, offset);
      //参数1:AtomicInteger对象 参数2:属性内存中的地址 参数3:内存中的值 参数4:新值(变更的值)
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
  }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值