【JUC并发】2. 不可不说的“锁”事,种类繁多,如何一一突破?

目录

一、根据不同维度,分类一:

1. 按照锁的粒度分

细粒度锁

粗粒度锁

2. 按照锁的共享方式分

共享锁

排他锁

3. 按照锁的实现方式分

自旋锁

阻塞锁

二、根据不同维度,分类二:

1. 悲观锁和乐观锁

2. 可重入锁和非可重入锁

3. 公平锁和非公平锁

4. 独占锁和共享锁

三、根据不同维度,分类三:

1. 偏向锁(Biased Locking)

2. 轻量级锁(Lightweight Locking)

3. 重量级锁(Heavyweight Locking)

4. 自旋锁(Spin Locking)

四、锁源码详解

        一、synchronized锁 实现原理、源码详解

A.synchronized 监视器的详细过程:

B. 底层的操作系统的 Mutex Lock实现原理

        二、ReentrantLock锁 实现原理、源码详解

A. ReentrantLock实现原理:

B. ReentrantLock源码详解

五、如何使用【锁】事

六、总结


在Java中,JUC(Java Util Concurrent)是一组用于实现多线程应用程序的实用工具类。JUC提供了许多线程安全的数据结构和工具类,其中包括锁。

一、根据不同维度,分类一:

1. 按照锁的粒度分

  • 细粒度锁

    细粒度锁是指锁定的范围非常小,只锁定共享资源的一小部分。这种锁可以减少线程之间的竞争,从而提高并发性能。Java中的ReentrantLock就是一种细粒度锁。

  • 粗粒度锁

    粗粒度锁是指锁定的范围比较大,锁住了整个共享资源。这种锁会导致线程之间的竞争增加,从而降低并发性能。Java中的synchronized就是一种粗粒度锁。

2. 按照锁的共享方式分

  • 共享锁

    共享锁是指多个线程可以同时获取该锁,用于读取共享资源。Java中的ReentrantReadWriteLock就是一种支持共享锁的锁。

  • 排他锁

    排他锁是指只有一个线程可以获取该锁,用于修改共享资源。Java中的synchronized和ReentrantLock都是一种排他锁。

3. 按照锁的实现方式分

  • 自旋锁

    自旋锁是指当获取锁失败时,线程不会进入阻塞状态,而是在一个循环中不断尝试获取锁。Java中的AtomicInteger就是一种自旋锁。

  • 阻塞锁

    阻塞锁是指当获取锁失败时,线程会进入阻塞状态等待锁释放。Java中的synchronized和ReentrantLock都是一种阻塞锁。

二、根据不同维度,分类二:

1. 悲观锁和乐观锁

悲观锁是一种独占锁,在对共享资源进行操作时,悲观锁会认为其他线程会对该资源进行修改,因此会将该资源锁定,直到操作完成后才会释放锁。乐观锁则是一种非独占锁,在对共享资源进行操作时,乐观锁会认为其他线程不会对该资源进行修改,因此不会对该资源进行加锁,而是在操作完成后进行数据版本的比较,如果版本一致则操作成功,否则需要进行重试。

// 悲观锁
synchronized (obj) {
    // 对共享资源进行操作
}

// 乐观锁
AtomicInteger value = new AtomicInteger(0);
value.compareAndSet(0, 1);

2. 可重入锁和非可重入锁

可重入锁是一种支持重复加锁的锁,即同一个线程可以多次获取同一个锁,而不会被锁死。非可重入锁则是一种不支持重复加锁的锁,如果同一个线程多次获取同一个锁,则会被锁死。

// 可重入锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 对共享资源进行操作
} finally {
    lock.unlock();
}

// 非可重入锁
Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
try {
    // 对共享资源进行操作
} finally {
    semaphore.release();
}

3. 公平锁和非公平锁

公平锁是一种按照线程等待的时间来获取锁的锁,即等待时间最长的线程会最先获取锁。非公平锁则是一种不按照线程等待时间来获取锁的锁,即线程可以在任何时刻获取锁,不考虑等待时间。

// 公平锁
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
try {
    // 对共享资源进行操作
} finally {
    lock.unlock();
}

// 非公平锁
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
try {
    // 对共享资源进行操作
} finally {
    lock.unlock();
}

4. 独占锁和共享锁

独占锁是一种只能被一个线程获取的锁,可以用于保护共享资源的独占访问。共享锁则是可以被多个线程同时获取的锁,可以用于保护共享资源的共享访问。

// 独占锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
try {
    // 对共享资源进行独占访问的操作
} finally {
    lock.writeLock().unlock();
}

// 共享锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
    // 对共享资源进行共享访问的操作
} finally {
    lock.readLock().unlock();
}

三、根据不同维度,分类三:

1. 偏向锁(Biased Locking)

  • 偏向锁是一种针对单线程执行的优化手段,当一个线程获取了锁后,会在对象头上记录这个线程ID,并设置偏向标志位。当这个线程再次获取同一把锁时,就不需要再次竞争,直接获得锁。只有当其他线程尝试获取锁时,才会撤销偏向锁。偏向锁的目的是减少无竞争情况下的锁操作,提高程序性能。可以使用 -XX:+UseBiasedLocking 参数打开偏向锁机制。

2. 轻量级锁(Lightweight Locking)

  • 轻量级锁是针对多个线程交替执行的情况进行的优化。当一个线程获取锁时,会在对象头上记录锁的标志位和线程ID,然后通过CAS操作尝试将对象头中的Mark Word替换为指向线程栈中锁记录的指针。如果操作成功,当前线程就获得了锁。如果CAS操作失败,表示有其他线程竞争锁,当前线程就进入自旋等待,尝试获取锁。当自旋次数超过阈值或者其他线程成功获取了锁,当前线程就会升级为重量级锁。轻量级锁的目的是减少多个线程竞争同一把锁时的锁操作,提高程序性能。

3. 重量级锁(Heavyweight Locking)

  • 重量级锁是针对多个线程竞争同一把锁时进行的优化。当一个线程获取锁时,会进入阻塞状态,此时操作系统会将线程挂起,直到其他线程释放锁。重量级锁的目的是确保同一时刻只有一个线程能够访问共享资源,保证数据的正确性。

4. 自旋锁(Spin Locking)

  • 自旋锁是一种特殊的锁,当一个线程获取锁时,如果发现其他线程正在使用锁,就会进入自旋等待,不会阻塞线程。当其他线程释放锁时,当前线程就可以立即获取锁,避免了线程的阻塞和唤醒开销。自旋锁的目的是减少线程阻塞和唤醒的开销,提高程序性能。

四、锁源码详解

        一、synchronized锁 实现原理、源码详解

  • synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质上又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要由用户态转换到核心态,这个成本非常高,状态之间的切换需要相对比较长的时间,这就是为什么使用 synchronized 会导致线程阻塞的原因。

A.synchronized 监视器的详细过程:

  1. 在进入同步代码块之前,线程会尝试获取锁。如果锁没有被其他线程持有,则这个线程获取锁并进入同步代码块;否则,这个线程就被阻塞,直到锁被其他线程释放为止。
  2. 当线程获取到锁并进入同步代码块时,会在对象头中设置锁的标记位,表示这个对象被锁定了。
  3. 当线程执行完同步代码块时,会释放锁,并清除对象头中的锁标记位,表示这个对象已经被释放了。
  4. 当线程被阻塞时,它会进入对象的等待队列中,等待被唤醒。当锁被释放时,等待队列中的线程会被唤醒,并竞争锁。

synchronized 的实现涉及到 Java 对象头的结构,具体可以参考 JDK 源码中的 Object 类的头文件定义:

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }

    /**
     * The synchronization status of this object.
     */
    private transient volatile int syncStatus;
    ...
}

其中的 syncStatus 就是用来存储锁状态的字段。在 HotSpot JVM 中,对象头的结构如下:

|--------------|--------------|--------------|--------------|
|    Mark Word  |    Class     |    Array     |    Padding   |
|--------------|--------------|--------------|--------------|

 其中的 Mark Word 中就包含了锁的标记位信息。具体结构可以参考下面的图示:

|--------------------------|--------------------------|
|      unused:25-31        |    identity_hashcode:1   |
|--------------------------|--------------------------|
|    unused:1-3 | age:4    |    biased_lock:1         |
|--------------------------|--------------------------|
|         lock:2           |    GC_state:2            |
|--------------------------|--------------------------|
|    ptr_to_lock_record:32 |    ptr_to_heavyweight_mon:32|
|--------------------------|--------------------------|

其中的 biased_lock 就是用来表示偏向锁状态的标记位,在没有竞争的情况下可以避免互斥操作从而提高性能。当有其他线程尝试获取锁时,偏向锁就会升级为轻量级锁或重量级锁。

B. 底层的操作系统的 Mutex Lock实现原理

synchronized 是 Java 中最基本的互斥同步手段,它的实现基于对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质上又是依赖于底层的操作系统的 Mutex Lock 来实现的。具体过程如下

1. 在编译阶段,Java 编译器会在代码中的每个同步块(synchronized 块)的前后分别插入 monitorenter 和 monitorexit 指令,这两个指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。

synchronized (obj) {
    // 同步块内容
}

编译后的字节码如下:

// monitorenter 指令,获取锁
// 进入 synchronized 块之前
monitorenter obj

// 同步块内容
// ...

// monitorexit 指令,释放锁
// 离开 synchronized 块之后
monitorexit obj

2. monitorenter 和 monitorexit 指令需要依赖于底层操作系统的 Mutex Lock 来实现对对象的加锁和解锁。在 Java 虚拟机实现中,每个对象都与一个监视器(monitor)关联,当一个线程试图获取对象的监视器时,该线程会进入到对象的监视器队列中等待,直到获取到该对象的监视器。获取到监视器的线程可以执行同步块中的代码,执行完毕后,该线程会释放该对象的监视器,以便其他等待线程可以获取该监视器并执行同步块中的代码。

3. synchronized 的实现还涉及到锁的升级和降级。在 Java 6 及之前的版本中,synchronized 的实现是基于重量级锁(也称为互斥量)来实现的,这种锁的获取和释放都需要进行用户态和内核态之间的切换,因此效率比较低。从 Java 6 开始,synchronized 的实现引入了锁的升级和降级机制,具体来说,当一个线程获取锁时,synchronized 会先尝试使用偏向锁(适用于只有一个线程访问对象的情况),如果偏向锁获取失败,则尝试使用轻量级锁(适用于有多个线程交替访问对象的情况),最后才会使用重量级锁。在锁的释放过程中,synchronized 也会根据情况将锁从重量级锁降级为轻量级锁或偏向锁,以提高程序的执行效率。

        二、ReentrantLock实现原理、源码详解

ReentrantLock是Java中的一个独占锁,可以用于实现线程同步。在Java中,synchronized关键字也可以用于线程同步,但是synchronized关键字有很多限制,比如无法中断、无法尝试获取锁等,而ReentrantLock则可以解决这些限制。

A. ReentrantLock实现原理:

ReentrantLock是通过Java中的AQS(AbstractQueuedSynchronizer)实现的,AQS是一个抽象类,提供了一个框架,可以用于实现同步器。ReentrantLock就是通过继承AQS实现的。

ReentrantLock是Java中的一种可重入互斥锁,它具有与synchronized关键字相同的基本行为和语义,但提供了更多的灵活性和可扩展性。ReentrantLock实现了Lock接口,它提供了一组丰富的特性,例如锁投票、定时锁等待和可中断锁等待。与synchronized关键字相比,ReentrantLock提供了更丰富的功能,但使用起来也更复杂一些。

ReentrantLock实现原理的核心是AQS(AbstractQueuedSynchronizer),它是实现锁和相关同步器的关键类。AQS使用一个FIFO队列来管理线程,当线程请求锁的时候,如果锁已经被其他线程占用了,那么该线程就会被加入到等待队列中,等待锁释放。当锁释放的时候,AQS会从等待队列中取出第一个线程,并让它获取到锁。

ReentrantLock内部维护了一个state变量,表示锁的状态。当state为0时,表示锁是未锁定状态,当state大于0时,表示锁已经被某个线程占用了。当一个线程请求锁的时候,如果state为0,那么该线程就可以获取到锁,并将state设置为1;如果state大于0,那么该线程就会被加入到等待队列中,等待锁释放。当线程释放锁的时候,它会将state设置为0,并从等待队列中唤醒一个等待线程。

ReentrantLock中有两种锁,分别是公平锁和非公平锁。公平锁是指线程获取锁的顺序与它们加入等待队列的顺序相同,而非公平锁则不保证这个顺序。在ReentrantLock中,默认使用非公平锁。

B. ReentrantLock源码详解

ReentrantLock中有一个Sync类,它继承了AQS类,并实现了lock、tryAcquire和tryRelease等方法。Sync类是ReentrantLock的内部类,它有两个子类,分别是NonfairSync和FairSync。NonfairSync是非公平锁的实现,FairSync是公平锁的实现。

在ReentrantLock的构造函数中,我们可以选择使用公平锁还是非公平锁。如果我们不传入参数,默认使用非公平锁。

ReentrantLock中的lock方法,就是调用Sync类中的lock方法,它会先尝试获取锁,如果获取失败,就会加入到等待队列中,并被阻塞,直到锁被释放。在Sync类的lock方法中,会调用AQS类的acquire方法。

ReentrantLock中的tryLock方法,就是调用Sync类中的nonfairTryAcquire方法,它会尝试获取锁,如果获取成功,返回true,否则返回false。在Sync类的nonfairTryAcquire方法中,会先判断当前锁是否被占用,如果没有被占用,就尝试获取锁。如果当前锁已经被占用,就返回false。

ReentrantLock中的unlock方法,就是调用Sync类中的release方法,它会释放锁,并唤醒等待队列中的一个线程。在Sync类的release方法中,会调用AQS类的release方法。

ReentrantLock中的newCondition方法,就是调用Sync类中的newCondition方法,它会返回一个Condition对象,用于实现条件变量。在Sync类的newCondition方法中,会创建一个ConditionObject对象,它是AQS类的内部类,用于实现条件变量。

除此之外,ReentrantLock还提供了很多其他方法,比如isHeldByCurrentThread、getHoldCount、isLocked等,用于获取锁的状态信息。

下面是ReentrantLock的源码及其注释说明:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;

    // AQS对象,用于实现锁和相关同步器
    private final Sync sync;

    // 构造函数,默认创建非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    // 构造函数,根据fair参数创建公平锁或非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    // 获取锁
    public void lock() {
        sync.lock();
    }

    // 获取锁,可中断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    // 尝试获取锁,不会阻塞
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    // 带超时时间的尝试获取锁
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    // 释放锁
    public void unlock() {
        sync.release(1);
    }

    // 返回Condition对象
    public Condition newCondition() {
        return sync.newCondition();
    }

    // 获取锁的当前持有者线程
    public Thread getOwner() {
        return sync.getOwner();
    }

    // 获取当前等待获取锁的线程数
    public int getQueueLength() {
        return sync.getQueueLength();
    }

    // 判断当前线程是否持有锁
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }

    // 判断锁是否被持有
    public boolean isLocked() {
        return sync.isLocked();
    }

    // 非公平锁实现类
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        // 尝试获取锁(非公平)
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    // 公平锁实现类
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        // 尝试获取锁(公平)
        final boolean fairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    // Sync是ReentrantLock的核心实现类,它继承了AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        // 获取锁
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        // 尝试获取锁(公平)
        abstract boolean fairTryAcquire(int acquires);

        // 尝试获取锁(非公平)
        abstract boolean nonfairTryAcquire(int acquires);

        // 尝试释放锁
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        // 返回Condition对象
        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // 获取锁的当前持有者线程
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        // 获取当前等待获取锁的线程数
        final int getQueueLength() {
            return getQueueLength();
        }

        // 判断当前线程是否持有锁
        final boolean isHeldExclusively() {
            return Thread.currentThread() == getExclusiveOwnerThread();
        }
    }
}

ReentrantLock源码中,Sync是ReentrantLock的核心实现类,它继承了AQS。ReentrantLock分为公平锁和非公平锁两种实现方式,分别对应FairSync和NonfairSync两个内部类。在lock()方法中,如果state为0,那么就将state设置为1,并将当前线程设置为锁的持有者;否则就调用acquire()方法将当前线程加入到等待队列中,等待锁释放。在unlock()方法中,先判断当前线程是否持有锁,如果不是则抛出IllegalMonitorStateException异常;否则就将state减去releases,并将锁的持有者线程设置为null。在非公平锁实现类NonfairSync中,尝试获取锁的方法nonfairTryAcquire()会直接获取锁,而不会判断等待队列中是否有线程在等待。在公平锁实现类FairSync中,尝试获取锁的方法fairTryAcquire()会先判断等待队列中是否有线程在等待,如果有则不会获取锁,而是将当前线程加入到等待队列中。

五、如何使用【锁】事

1. ReentrantLock:可重入锁,支持公平和非公平两种获取锁的方式。使用tryLock()方法可以尝试获取锁,避免死锁。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 获得锁后执行的代码
} finally {
    lock.unlock();
}

2. ReentrantReadWriteLock:可重入读写锁,支持多个线程同时读取,但只能有一个线程写入。读锁和写锁是互斥的。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
    // 获得读锁后执行的代码
} finally {
    lock.readLock().unlock();
}

lock.writeLock().lock();
try {
    // 获得写锁后执行的代码
} finally {
    lock.writeLock().unlock();
}

3. StampedLock:乐观锁,用于读多写少的场景。读锁和写锁是互斥的,但使用tryOptimisticRead()方法可以尝试获取一个乐观锁,避免阻塞。

StampedLock lock = new StampedLock();
long stamp = lock.readLock();
try {
    // 获得读锁后执行的代码
} finally {
    lock.unlock(stamp);
}

stamp = lock.writeLock();
try {
    // 获得写锁后执行的代码
} finally {
    lock.unlock(stamp);
}

4. Semaphore:信号量,用于控制同时访问某个资源的线程数量。

Semaphore semaphore = new Semaphore(10); // 允许同时访问的线程数量为10
semaphore.acquire(); // 获取许可
try {
    // 执行需要许可的代码
} finally {
    semaphore.release(); // 释放许可
}

5. CountDownLatch:倒计时门闩,用于等待多个线程完成某个任务。

CountDownLatch latch = new CountDownLatch(5); // 需要等待5个线程完成
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown(); // 完成任务后调用countDown()方法
    }).start();
}

latch.await(); // 等待所有线程完成任务

6. CyclicBarrier:循环屏障,用于等待多个线程到达某个状态后再同时执行。

CyclicBarrier barrier = new CyclicBarrier(5); // 需要等待5个线程
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        // 执行任务
        try {
            barrier.await(); // 等待所有线程到达屏障
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
        // 所有线程到达屏障后执行的代码
    }).start();
}

六、总结

JUC(Java.util.concurrent)提供了多种锁机制,包括:

  1. ReentrantLock:一个可重入的互斥锁,具有与 synchronized 相同的并发性和内存语义。与 synchronized 不同的是,它具有可中断锁获取、可轮询锁和定时锁等特性。

  2. ReentrantReadWriteLock:一个可重入的读写锁,它允许多个线程同时读取共享数据,但对于写操作是互斥的。与 ReentrantLock 相比,它可以提高读操作的并发性能。

  3. StampedLock:一个可重入的互斥锁,支持三种模式:写锁、悲观读锁和乐观读锁。与 ReentrantReadWriteLock 相比,它在读操作时不会阻塞写操作,提高了并发性能。

  4. Condition:与 Lock 配合使用的条件变量,可以实现等待/通知模式。

  5. Semaphore:一个计数信号量,可以用来限制同时访问某个共享资源的线程数。

  6. CountDownLatch:一个同步工具类,允许一个或多个线程等待其他线程完成操作后再执行。

  7. CyclicBarrier:一个同步工具类,允许多个线程相互等待,直到所有线程都达到某个屏障点后再同时执行。

  8. Phaser:一个同步工具类,可以动态地控制线程的阶段,每个阶段完成后进行同步。

以上是 JUC 中常用的锁机制,不同的锁机制适用于不同的场景,开发人员应根据具体情况选择合适的锁机制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码上团建

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

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

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

打赏作者

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

抵扣说明:

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

余额充值