JAVA并发基础

线程创建与运行

  1. 继承Thread类
public class ThreadTest {
    //继承Thread类并重写run方法
    public static class MyThread extends Thread {
        override
        public void run ( ) {
            system.out.println ( "I am a child thread" ) ;
        }
    }
    public static void main (String [] args){
        //创建线程
        MyThread thread = new MyThread ( ) ;
        //启动线程
        thread.start ( );
    }
}
  1. 实现Runnable接口
public static class RunableTask implements Runnable{
    @override
    public void run () {
        system.out.println ( "I am a child thread" );
    }
}
public static void main(string[] args) throws InterruptedException {
    RunableTask task = new RunableTask();
    new Thread(task) .start ();
    new Thread (task).start () ;
}
  1. 使用FutureTask
//创建任务类,类似Runable
public static class CallerTask implements Callable<String>{
    @Override
    public string call() throws Exception {
        return "hello";
    }

    public static void main (String[] args) throws InterruptedException {//创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask ());//启动线程
        new Thread(futureTask).start();
        try {
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            system.out.println (result);
        }catch(ExecutionException e){
            e.printstackTrace () ;
        }
    }
}

继承Thread的好处是在run方法中直接可以用this获取当前线程,缺点就是Java不支持多继承,而Runnable则没有这个限制,这两种方法都没有办法拿到返回值,使用FutureTask则可以。

Object中的方法

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面。

  1. wait()
    当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
  • 其他线程调用了该共享对象的notify()或者notifyAll()方法;
  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
    另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

synchronized(obj){
    while(条件不满足){
        obj.wait();
    }
}
  1. wait(long timeout)函数
    该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和 wait方法效果一样,因为在 wait方法内部就是调用了wait(O)。需要注意的是,如果在调用该函数时,传递了一个负的timeout则会抛出IllegalArgumentException异常。
  2. notify()函数
    一个线程调用共享对象的notify)方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
    此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
    类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。
  3. notifyAll()函数
    不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

等待线程执行终止的join方法

有时候我们会遇到一个场景,就是需要等待某一个线程完成后才能继续往下执行,这个时候就可以使用Thread的join方法。线程A调用了线程B的join方法之后,就会在调用处阻塞直到线程B执行完毕。
线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断了线程A时,线程A会抛出InterruptedException异常而返回。
调用join方法不会释放线程持有的锁资源。

让线程休眠的sleep方法

Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread 的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。

让出CPU执行权yield方法

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。
操作系统为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU的那个线程来获取CPU执行权。
总结: sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程中断

  1. void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
  2. boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
public boolean isInterrupted ( ) {
    //传递false,说明不清除中断标志
    return isInterrupted (false);
}
  1. boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。在interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
public static boolean interrupted () {
    //清除中断标志
    return currentThread().isInterrupted (true);
}

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力的干预下这些线程会一直相互等待下去。
产生死锁有四个必要条件:

  1. 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  3. 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  4. 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的TO正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被TO占用的资源。

死锁例子:

public class DeadLockTest2 {
    //创建资源
    private static object resourceA = new Object ();
    private static object resourceB = new Object ();

    public static void main (string[ ] args){
        //创建线程A
        Thread threadA = new Thread (new Runnable() {
            public void run() {
                synchronized(resourceA) {
                    system.out.println(Thread.currentThread () + " get ResourceA" ) ;
                    try {
                        Thread.sleep (1000) ;
                    }catch (InterruptedException e){
                        e.printstackTrace();
                    }
                    System.out.println(Thread.currentThread () + "waiting get sourceB");
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread () + "get esourceB");
                    }
                }
            }
        });
        //创建线程B
        Thread threadB = new Thread(new Runnable () {
            public void run () {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread ( ) + " get ResourceB" );
                    try {
                        Thread.sleep (1000) ;
                    } catch (InterruptedException e){
                        e.printstackTrace ( ) ;
                    }
                    System.out.println(Thread.currentThread () + "waiting get esourceA" );
                    synchronized (resourceA) {
                        system.out.println(Thread.currentThread () + "get ResourceA");
                    }
                }
            };
        });
        //启动线程
        threadA.start();
        threadB.start();
     }
}

避免线程死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。

守护线程与用户线程

Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
开启Java守护线程的代码如下:

public static void main (string [ ] args) {
    Thread daemonThread = new Thread (new Runnable(){
        public void run () {
        }
    });
    //设置为守护线程
    daemonThread.setDaemon(true);
    daemonThread.start();
}

ThreadLocal

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

实现原理

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  1. set
public void set(T value) {
    //获得当前线程
    Thread t = Thread.currentThread();
    //获得当前线程的threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //不为null就直接添加对应的key value。
        map.set(this, value);
    else
        //当前线程的threadLocals为null,就创建一个
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    //返回线程的threadLocals属性
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    //初始化一个ThreadLocalMap
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  1. get
public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //获得当前线程的threadLocals属性
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获得对应的Entry,之后就可以拿到value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  1. remove
public void remove() {
    //获得当前线程的threadLocals属性
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

InheritableThreadLocals类

变量被设置在父线程中,子线程是获取不到的,为了解决这个问题,InheritableThreadLocals应运而生,InheritableThreadLocals继承至ThreadLocal,其实现了让子线程可以访问到父线程设置的本地变量。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    //获得当前线程的inheritableThreadLocals属性,而不是threadLocals属性。
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    //初始化的是inheritableThreadLocals属性
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

通过InheritableThreadLocals对象实例操作的是Thread的inheritableThreadLocals属性。
在新建子线程的时候,子线程会被父线程的inheritableThreadLocals属性保存的key value拷贝一份到自己的inheritableThreadLocals属性中。

ThreadLocal内存泄漏问题

ThreadLocalMap中保存的key是指向ThreadLocal对象的指针是一个弱引用,value是对应的值。
在这里插入图片描述
为什么没有用强引用?
如果用强引用在ThreadLocal对象实例没有被其他地方引用的时候,由于ThreadLocalMap中还保持这这个对象的强引用,那么这个对象就不会被回收,但是已经访问不到了,出现内存泄露。
使用弱引用仍然有内存泄露的现象
使用了弱引用之后,在ThreadLocal对象实例没有被其他地方引用的时候,发生GC会把这个ThreadLocal对象给回收,但是key回收了,value还是没有处理,因此存在value的内存泄露。
使用完之后,手动remove防止内存泄露

并发和并行

  • 并发:CPU会把一个时间段分成多个时间片,多个线程可以获得这些时间片执行自己的代码,这段时间内就称为多线程并发。一个CPU在一段时间内根据时间片分配切换执行多个线程。
  • 并行:多个CPU在一个时间点执行多个线程。

为什么要多线程并发编程

我们在写程序的时候经常遇到需要等待IO操作完毕在继续执行的情况,这个时候线程会阻塞,假如没有并发,那这个时候CPU就是干等着,有并发CPU就可以在一个线程阻塞的时候切换执行其他线程。

CAS

CAS(Compare and Swap,比较并交换)是一个CPU指令,其语义为:“如果地址V上的值和期望的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS存在的缺点

  1. CPU开销过大:由于是乐观锁,因此一般会用自旋来检测CAS操作是否成功,当操作没有成功时,线程会反复尝试更新一个变量,这样会导致一直消耗CPU。
  2. ABA问题:假设要更新一个A值为B,这时候有另一个线程,已经把A改为了C,然后又改成了A,那么操作还是会成功,也就是说CAS没办法发现其实已经有其他线程进行了更新。(就像一个人倒了一杯水,然后走开了,这时候这杯水被另一个人喝完了,然后他又重新到了一杯,放回原处,此时原本这杯水的主人并不会发现这杯水已经被换过了)
  3. 总线风暴:在java中使用unsafe实现cas,而其底层由cpp调用汇编指令实现的,如果是多核cpu是使用lock cmpxchg指令,单核cpu 使用compxch指令。如果在短时间内产生大量的cas操作在加上 volatile的嗅探机制则会不断地占用总线带宽,导致总线流量激增,就会产生总线风暴。 总之,就是因为volatile 和CAS 的操作导致BUS总线缓存一致性流量激增所造成的影响。

Unsafe

Unsafe类是在sun.misc包下,不属于Java标准。获得Unsafe必须通过反射,因为Unsafe.getUnsafe会进行校验,不是启动类加载器加载的类对象没有权限执行这个方法拿到Unsafe,Unsafe提供了以下的功能:

  1. 内存管理:包括分配内存、释放内存等。
    该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。
  2. 非常规的对象实例化
    allocateInstance()方法提供了另一种创建实例的途径。通常我们可以用new或者反射来实例化对象,使用allocateInstance()方法可以直接生成对象实例,且无需调用构造方法和其它初始化方法。
    这在对象反序列化的时候会很有用,能够重建和设置final字段,而不需要调用构造方法。
  3. 操作类、对象、变量。
    这部分包括了staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)等方法。
    通过这些方法我们可以获取对象的指针,通过对指针进行偏移,我们不仅可以直接修改指针指向的数据(即使它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。
  4. 数组操作
    这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。
    由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。
  5. 多线程同步。包括锁机制、CAS操作等。
    这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。
    其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。
    Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。compareAndSwap方法是原子的,可以避免繁重的锁机制,提高代码效率。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。
  6. 挂起与恢复。
    这部分包括了park、unpark等方法。
    将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
  7. 内存屏障。
    这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。
    loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。

伪共享

CPU的缓存是以缓存行(cache line)为单位进行缓存和主内存的交互,当多个线程修改不同变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。
例如现在有变量A,变量B属于一个缓存行,线程A LOCK这个变量A所在的缓存行,如果线程B要对变量B进行加锁就得等线程A释放缓存行上的锁。
或者说线程A修改了变量A,这时候线程B中的缓存行就会失效,要从主内存中重新读取。

Java中的锁

  1. 悲观锁:认为每次使用资源都会存在竞争,所以先去上锁。
  2. 乐观锁:认为不会存在竞争,只在最后去对数据进行判断,CAS。
  3. 公平锁:竞争锁的线程会进行排序,按照顺序一个一个的拿到锁。
  4. 非公平锁:如果有一个新建的线程(还没有入队列),它会和队列头部的线程竞争获得锁。
  5. 独占锁:任何时候只有一个线程可以获得锁。
  6. 共享锁:放宽了加锁的条件,允许多个线程同时进行读操作。
  7. 可重入锁:获得锁的线程可以再一次获得这把锁,记录获得锁的次数,释放时要保证释放了相同的次数。
  8. 自旋锁:获得锁失败的时候,不会立刻阻塞,而是去循环获取这个锁。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程是指在Java程序中使用多线程实现并发执行的编程技术。它能有效利用多核处理器的优势,提升程序的性能和响应能力。以下是Java并发编程的基础知识: 1. 线程与进程:Java中的线程是程序中执行的最小单位,线程共享进程的资源,包括内存空间和文件等。多线程可以同时执行不同的任务,相比单线程能更高效地利用系统资源。 2. 线程创建:Java中创建线程有两种方式,一种是继承Thread类,实现run()方法;另一种是实现Runnable接口,重写run()方法。通过调用start()方法启动线程。 3. 线程同步:多个线程在访问共享资源时可能会产生竞争条件,可能会导致数据不一致或者出现死锁等问题。通过使用同步机制来保证线程安全,例如使用synchronized关键字实现对共享资源的互斥访问。 4. 线程通信:线程之间可以通过共享变量来进行通信。使用wait()、notify()和notifyAll()方法实现线程的等待和唤醒。 5. 线程池:线程池是一种管理线程的机制,可以有效控制线程的数量和复用线程资源,避免频繁创建销毁线程的开销。 6. 并发容器:Java提供了一些线程安全的数据结构,如ConcurrentHashMap和ConcurrentLinkedQueue等,用于在多线程环境下安全地操作数据。 7. 原子操作:Java提供了一些原子操作类,如AtomicInteger和AtomicLong等,它们能够保证对共享数据的操作是原子的,不会发生数据不一致的情况。 8. 同步工具类:Java提供了一些同步工具类,如CountDownLatch和CyclicBarrier等,用于控制线程的执行顺序和线程之间的同步。 以上是Java并发编程的基础知识,掌握了这些知识可以更好地利用多线程来提高程序的性能和并发能力。同时也需要注意并发编程可能带来的线程安全问题,合理使用同步机制和并发容器等工具类来保证程序的正确运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值