Java编程思想笔记——并发2

共享受限资源

不正确地访问资源

EvenChecker,消费者任务。
为了将EvenChecker与要实验的各种类型的生成器解耦,将创建一个名为IntGenerator的抽象类:

public abstract class IntGenerator {
    private volatile boolean canceled = false;

    public abstract int next();

    // Allow this to be canceled:
    public void cancel() {
        canceled = true;
    }

    public boolean isCanceled() {
        return canceled;
    }
} 

canceled标识是boolean类型的,所以他是原子性的。为了保证可视性,canceled标志是volatile的。

public class EvenChecker implements Runnable {
    private IntGenerator generator;
    private final int id;

    public EvenChecker(IntGenerator g, int ident) {
        generator = g;
        id = ident;
    }

    @Override
    public void run() {
        while (!generator.isCanceled()) {
            int val = generator.next();
            if (val % 2 != 0) {
                System.out.println(val + " not even!");
                generator.cancel(); // Cancels all EvenCheckers
            }
        }
    }

    // Test any type of IntGenerator:
    public static void test(IntGenerator gp, int count) {
        System.out.println("Press Control-C to exit");
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < count; i++) {
            exec.execute(new EvenChecker(gp, i));
        }
        exec.shutdown();
    }

    // Default value for count:
    public static void test(IntGenerator gp) {
        test(gp, 10);
    }
}

本例中可以被撤销的类不是Runnable,而所有依赖于IntGenerator对象的EvenChecker任务来测试它,以查看他是否已经被撤销,通过这种方式,共享公共资源(IntGenerator)的任务可以观察该资源的终止型号,可以消除所谓竞争条件。
test()方法通过启动大量使用相同的IntGenerator的EvenChecker,设置并执行对任何类型的IntGenerator的测试。如果IntGenerator引发失败,那么test()将报告它并返回,否则,必须按下Control-C来终止它。

public class EvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  @Override
  public int next() {
    ++currentEvenValue; // Danger point here!
    ++currentEvenValue;
    return currentEvenValue;
  }
  public static void main(String[] args) {
    EvenChecker.test(new EvenGenerator());
  }
} /*
Press Control-C to exit
89476993 not even!
89476993 not even!
*/

一个任务有可能在另一个任务执行第一个对currentEvenValue的递增操作之后,但是没有执行第二个操作之前,调用next()方法。这将使这个值处于不恰当的状态。为了证明这是可能发生的,EvenChecker.test()创建了一组EvenChaecker对象,以连续的读取并输出同一个EvenGenerator,并测试检查每个数值是否都是偶数。如果不是,就会报告错误,而程序也将关闭。
这个程序最终将失败,因为各个EvenChecker任务在EvenGenerator处于不恰当状态时,仍能访问其中信息。如果希望更快的发现失败,可以尝试着将对yield()的调用放置到第一个和第二个递增操作之间。
递增程序自身也需要多个步骤,并且在递增过程中任务可能会被线程机制挂起——也就是说,在Java中,递增不是原子性操作。因此,如果不保护任务,即使单一的递增也不是安全的。

解决共享资源竞争

永远不知道一个线程何时在运行。

资源加锁,基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,这种机制常常称为互斥量(mutex)。

Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
要控制对共享资源的访问,得先把它包装进一个对象。把所有要访问这个资源的方法标记为synchronized。如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有调用类中任何标记为synchronized方法的线程都会被阻塞。
注意在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
一个任务可以多次获得对象的锁。在任务第一次给对象加锁时候,计数器变为1(JVM负责跟踪对象被加锁的次数)。每当这个相同的任务在这个对象上获得锁,计数都会递增。每当任务离开一个synchronized方法,计数递减,当计数为0时,锁被完全释放,此时别的任务就可以使用此资源。
针对每个类,也有一个锁,所以synchronized static方法可以在类的范围内防止对static数据的并发访问。、
同步规则:
如果正在写一个变量,他可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个程序写过的变量,那么就必须同步,并且,读写线程都必须用相同的监视器锁同步。

同步控制EvenGenerator
public class SynchronizedEvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;

    @Override
    public synchronized int next() {
        ++currentEvenValue;
        Thread.yield(); // Cause failure faster
        ++currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EvenChecker.test(new SynchronizedEvenGenerator());
    }
}

Thread.yield()以提高currentEvenValue是奇数装填时上下文切换的可能性。因为互斥可以防止多个任务同时进入临界区,所以这不会产生任何失败。
第一个进入next()的任务将获得锁,任何其他试图获取锁的任务都将从其开始尝试之时被阻塞,直至第一个任务释放锁。通过这种方式,任何时刻只有一个任务可以通过有互斥量看护的代码。

使用显示的Lock对象

Java SE5的java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显示的互斥机制。Lock对象必须被显式的创建、锁定和释放。缺乏优雅性,但是更加灵活:

public class MutexEvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;
    private Lock lock = new ReentrantLock();

    @Override
    public int next() {
        lock.lock();
        try {
            ++currentEvenValue;
            Thread.yield();
            ++currentEvenValue;
            return currentEvenValue;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        EvenChecker.test(new MutexEvenGenerator());
    }
}

MutexEvenGenerator添加了一个被互斥调用的锁,并使用lock()和unlock()方法在next()内部创建了临界资源。
尽管try-finally所需的代码比synchronize关键字要多,但是这也代表了显式的Lock对象的优点之一。如果在使用synchronize时,某些事物失败了,那么就会抛出一个异常。但是没有机会去做任何清理工作,以维护系统使其处于良好状态。
用synchronize关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,必须使用concurrent类库:

public class AttemptLocking {
    private ReentrantLock lock = new ReentrantLock();

    public void untimed() {
        boolean captured = lock.tryLock();
        try {
            System.out.println("tryLock(): " + captured);
        } finally {
            if (captured) {
                lock.unlock();
            }
        }
    }

    public void timed() {
        boolean captured = false;
        try {
            captured = lock.tryLock(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            System.out.println("tryLock(2, TimeUnit.SECONDS): " +
                    captured);
        } finally {
            if (captured) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        final AttemptLocking al = new AttemptLocking();
        al.untimed(); // True -- lock is available
        al.timed();   // True -- lock is available
        // Now create a separate task to grab the lock:
        new Thread() {
            {
                setDaemon(true);
            }

            @Override
            public void run() {
                al.lock.lock();
                System.out.println("acquired");
            }
        }.start();
        Thread.yield(); // Give the 2nd task a chance
        al.untimed(); // False -- lock grabbed by task
        al.timed();   // False -- lock grabbed by task
    }
} /*
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2, TimeUnit.SECONDS): false
*/

ReentrantLock允许尝试着获取但最终未获取锁。在timed()中,做出了尝试去获取锁,该尝试可以在2秒之后失败。在main()中,作为匿名类而创建了一个单独的Thread,它将获取锁,这使得untimed()和timed()方法对某些事物将产生竞争。
显示的Lock对象在加锁和释放锁方面,赋予了更细粒度的控制力。

原子性与易变性

原子操纵不能被线程调度机制中断。
但是JVM可以将64位(long和double变量)的读写当作两个分离的32位操作来执行,这就可以在读取和写入之间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(字撕裂)。使用volatile关键字,就会获得原子性(简单的赋值与返回操作的)。
volatile关键字还确保了应用中的可视性。如果将一个域声明为volatile的,那么只要对这个域产生写操作,那么所有的读操作就都可以看到这个修改。即便使用了本地缓存,情况也是如此,volatile域会立即被写入到主存中,而读取操作就会发生在主存中。
i++;
i += 2;
在Java中,上面操作肯定不是原子性的:

// {Exec: javap -c Atomicity}

public class Atomicity {
  int i;
  void f1() { i++; }
  void f2() { i += 3; }
} /*
...
void f1();
  Code:
   0:        aload_0
   1:        dup
   2:        getfield        #2; //Field i:I
   5:        iconst_1
   6:        iadd
   7:        putfield        #2; //Field i:I
   10:        return

void f2();
  Code:
   0:        aload_0
   1:        dup
   2:        getfield        #2; //Field i:I
   5:        iconst_3
   6:        iadd
   7:        putfield        #2; //Field i:I
   10:        return
*/

每条指令都会产生一个get和put,他们之间还有一些其他的指令。因为在获取和放置之间,另一个任务可能会修改这个域,所以,这些操作不是原子性的。

public class AtomicityTest implements Runnable {
    private int i = 0;

    public int getValue() {
        return i;
    }

    private synchronized void evenIncrement() {
        i++;
        i++;
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while (true) {
            int val = at.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
} /*
191583767
*/

尽管return i确实是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取。除此之外,i也不是volatile的,还存在可视化问题。

一个产生序列数字的类。每当nextSerialNumber()被调用时,必须向调用者返回唯一的值:

public class SerialNumberGenerator {
  private static volatile int serialNumber = 0;
  public static int nextSerialNumber() {
    return serialNumber++; // Not thread-safe
  }
}

Java的递增操作不是原子性的。nextSerialNumber在没有同步的情况下对共享可变值进行了访问。
基本上,如果一个域可能会被多个任务访问,或者这些任务中至少有一个写入任务,就应该将这个域设置为volatile。将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入操作直接针对内存,而却没有被缓存。但是,volatile并不能对递增不是原子性操作,这一事实产生影响。

class CircularSet {
    private int[] array;
    private int len;
    private int index = 0;

    public CircularSet(int size) {
        array = new int[size];
        len = size;
        // Initialize to a value not produced
        // by the SerialNumberGenerator:
        for (int i = 0; i < size; i++) {
            array[i] = -1;
        }
    }

    public synchronized void add(int i) {
        array[index] = i;
        // Wrap index and write over old elements:
        index = ++index % len;
    }

    public synchronized boolean contains(int val) {
        for (int i = 0; i < len; i++) {
            if (array[i] == val) {
                return true;
            }
        }
        return false;
    }
}

public class SerialNumberChecker {
    private static final int SIZE = 10;
    private static CircularSet serials =
            new CircularSet(1000);
    private static ExecutorService exec =
            Executors.newCachedThreadPool();

    static class SerialChecker implements Runnable {
        @Override
        public void run() {
            while (true) {
                int serial =
                        SerialNumberGenerator.nextSerialNumber();
                if (serials.contains(serial)) {
                    System.out.println("Duplicate: " + serial);
                    System.exit(0);
                }
                serials.add(serial);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < SIZE; i++) {
            exec.execute(new SerialChecker());
        }
        // Stop after n seconds if there's an argument:
        if (args.length > 0) {
            TimeUnit.SECONDS.sleep(new Integer(args[0]));
            System.out.println("No duplicates detected");
            System.exit(0);
        }
    }
} /*
Duplicate: 8468656
*/

对基本类型的读取和赋值操作被认为是安全的原子性操作。但是,正如在AtomicityTest.java中看到的,当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问他们。对这个问题做出假设是棘手而危险的,最明智的做法就是遵循Brian的同步规则。

原子类

Java SE5引入了AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供了我原子性条件更新操作:
boolean compareAndSet(expectedValue, undateValue);
这些类被调整为可以使用在某些现代处理器上的可获得的,并且是在机器级别上的原子性。对于常规编程很少用,但是涉及性能调优时,大有用武之地:

public class AtomicIntegerTest implements Runnable {
    private AtomicInteger i = new AtomicInteger(0);

    public int getValue() {
        return i.get();
    }

    private void evenIncrement() {
        i.addAndGet(2);
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.err.println("Aborting");
                System.exit(0);
            }
        }, 5000); // Terminate after 5 seconds
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.execute(ait);
        while (true) {
            int val = ait.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

通过使用AtomicInteger而消除了synchronize关键字。

public class AtomicEvenGenerator extends IntGenerator {
  private AtomicInteger currentEvenValue = new AtomicInteger(0);
  @Override
  public int next() {
    return currentEvenValue.addAndGet(2);
  }
  public static void main(String[] args) {
    EvenChecker.test(new AtomicEvenGenerator());
  }
}

所有其他形式的同步再次通过使用AtomicInteger得到了根除。
Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊情况下才在自己代码中使用它们(通常依赖于锁)。

临界区

有时,只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,分离出来的代码段被称为临界区(critical section)。也是用synchronize建立:

synchronize(syncObject) {
}

也被成为同步控制块。在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。此外,演示如何把一个非保护类型的类,在其他类的保护和控制之下,应用于多线程的环境:

class Pair { // Not thread-safe
    private int x, y;

    public Pair(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Pair() {
        this(0, 0);
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void incrementX() {
        x++;
    }

    public void incrementY() {
        y++;
    }

    public String toString() {
        return "x: " + x + ", y: " + y;
    }

    public class PairValuesNotEqualException
            extends RuntimeException {
        public PairValuesNotEqualException() {
            super("Pair values not equal: " + Pair.this);
        }
    }

    // Arbitrary invariant -- both variables must be equal:
    public void checkState() {
        if (x != y)
            throw new PairValuesNotEqualException();
    }
}

// Protect a Pair inside a thread-safe class:
abstract class PairManager {
    AtomicInteger checkCounter = new AtomicInteger(0);
    protected Pair p = new Pair();
    private List<Pair> storage =
            Collections.synchronizedList(new ArrayList<Pair>());

    public synchronized Pair getPair() {
        // Make a copy to keep the original safe:
        return new Pair(p.getX(), p.getY());
    }

    // Assume this is a time consuming operation
    protected void store(Pair p) {
        storage.add(p);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException ignore) {
        }
    }

    public abstract void increment();
}

// Synchronize the entire method:
class PairManager1 extends PairManager {
    public synchronized void increment() {
        p.incrementX();
        p.incrementY();
        store(getPair());
    }
}

// Use a critical section:
class PairManager2 extends PairManager {
    public void increment() {
        Pair temp;
        synchronized (this) {
            p.incrementX();
            p.incrementY();
            temp = getPair();
        }
        store(temp);
    }
}

class PairManipulator implements Runnable {
    private PairManager pm;

    public PairManipulator(PairManager pm) {
        this.pm = pm;
    }

    public void run() {
        while (true)
            pm.increment();
    }

    public String toString() {
        return "Pair: " + pm.getPair() +
                " checkCounter = " + pm.checkCounter.get();
    }
}

class PairChecker implements Runnable {
    private PairManager pm;

    public PairChecker(PairManager pm) {
        this.pm = pm;
    }

    public void run() {
        while (true) {
            pm.checkCounter.incrementAndGet();
            pm.getPair().checkState();
        }
    }
}

public class CriticalSection {
    // Test the two different approaches:
    static void
    testApproaches(PairManager pman1, PairManager pman2) {
        ExecutorService exec = Executors.newCachedThreadPool();
        PairManipulator
                pm1 = new PairManipulator(pman1),
                pm2 = new PairManipulator(pman2);
        PairChecker
                pcheck1 = new PairChecker(pman1),
                pcheck2 = new PairChecker(pman2);
        exec.execute(pm1);
        exec.execute(pm2);
        exec.execute(pcheck1);
        exec.execute(pcheck2);
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            System.out.println("Sleep interrupted");
        }
        System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
        System.exit(0);
    }

    public static void main(String[] args) {
        PairManager
                pman1 = new PairManager1(),
                pman2 = new PairManager2();
        testApproaches(pman1, pman2);
    }
} /*
pm1: Pair: x: 15, y: 15 checkCounter = 272565
pm2: Pair: x: 16, y: 16 checkCounter = 3956974
*/

正如注释中注明的,Pair不是线程安全的,因为它的约束条件需要两个变量要维护成相同的值。此外,自增操作不是线程安全的,并且因为没有任何方法被标记为synchronize,所以不能保证一个Pair对象在多线程中不会被破坏。
至于PairManager类的结构,它的一些功能在基类中实现,并且其一个或多个抽象方法在派生类中定义,这种结构在设计模式中成为模版方法。

尽管每次运行的结果可能非常不同,但一般来说,对PairChecker的检查频率,PairManafer1.increment()不允许有PairManager2.increment()那么多。后者采用同步控制块进行同步,所以对象不加锁的时间更长。这也是宁愿使用同步控制块而不是整个方法进行同步控制的典型原因:使得其他线程能更多地访问。

可以使用显式的Lock对象来创建临界区:

class ExplicitPairManager1 extends PairManager {
    private Lock lock = new ReentrantLock();

    public synchronized void increment() {
        lock.lock();
        try {
            p.incrementX();
            p.incrementY();
            store(getPair());
        } finally {
            lock.unlock();
        }
    }
}

// Use a critical section:
class ExplicitPairManager2 extends PairManager {
    private Lock lock = new ReentrantLock();

    public void increment() {
        Pair temp;
        lock.lock();
        try {
            p.incrementX();
            p.incrementY();
            temp = getPair();
        } finally {
            lock.unlock();
        }
        store(temp);
    }
}

public class ExplicitCriticalSection {
    public static void main(String[] args) throws Exception {
        PairManager
                pman1 = new ExplicitPairManager1(),
                pman2 = new ExplicitPairManager2();
        CriticalSection.testApproaches(pman1, pman2);
    }
} /*
pm1: Pair: x: 15, y: 15 checkCounter = 174035
pm2: Pair: x: 16, y: 16 checkCounter = 2608588
*/
在其他对象上同步

synchronize块必须给定一个在其上同步的对象,最合理的方式是:synchronize(this)。如果获得了synchronize块上的锁,那么该对象其他的synchronize方法和临界区就不能被调用了。因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内。

有时必须在另一个对象上同步,如果这么做,就必须确保所有任务都在同一对象上同步的:

class DualSynch {
    private Object syncObject = new Object();

    public synchronized void f() {
        for (int i = 0; i < 5; i++) {
            print("f()");
            Thread.yield();
        }
    }

    public void g() {
        synchronized (syncObject) {
            for (int i = 0; i < 5; i++) {
                print("g()");
                Thread.yield();
            }
        }
    }
}

public class SyncObject {
    public static void main(String[] args) {
        final DualSynch ds = new DualSynch();
        new Thread() {
            public void run() {
                ds.f();
            }
        }.start();
        ds.g();
    }
} /*
g()
f()
g()
f()
g()
f()
g()
f()
g()
f()
*/

DualSync.f()(通过同步整个方法)在this同步,而g()有一个在syncObject上同步的synchronize快。因此,这两个同步相互独立的。通过在main()中创建调用f()的Thread对这一点进行了演示,因为main()线程是被用来调用g()的。从输出中可以看到,这两个方式在同时运行,因为任何一个方法都没有因为对另一个方法的同步而被阻塞。

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本次存储是一种自动化机制,可以为使用相同变量的每一个不同线程都创建不同的存储。因此,如果有5个线程都要使用变量x,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。
创建与管理本地线程可以有java.lang.ThreadLocal类实现:

class Accessor implements Runnable {
    private final int id;

    public Accessor(int idn) {
        id = idn;
    }

    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }

    public String toString() {
        return "#" + id + ": " +
                ThreadLocalVariableHolder.get();
    }
}

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value =
            new ThreadLocal<Integer>() {
                private Random rand = new Random(47);

                protected synchronized Integer initialValue() {
                    return rand.nextInt(10000);
                }
            };

    public static void increment() {
        value.set(value.get() + 1);
    }

    public static int get() {
        return value.get();
    }

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++)
            exec.execute(new Accessor(i));
        TimeUnit.SECONDS.sleep(3);  // Run for a while
        exec.shutdownNow();         // All Accessors will quit
    }
} /*
#0: 9259
#1: 556
#2: 6694
#3: 1862
#4: 962
#0: 9260
#1: 557
#2: 6695
#3: 1863
#4: 963
...
*/

ThreadLocal对象通常当作静态云存储。在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容,其中,get()方法将返回与其线程相关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。increment和get都不是synchronize的,因为ThreadLocal保证不会出现竞争条件。
当运行这个程序时,可以看到每个单独的线程都被分配了自己的存储,因为它们每个都需要跟踪自己的计数值,即便只有一个ThreadLocalVariableHolder对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值