java并发锁获取的方式_Java并发之(3):锁

锁是并发编程中的重要概念,用来控制多个线程对同一资源的并发访问,在支持并发的编程语言中都有体现,比如c++ python等。本文主要讲解Java中的锁,或者说是重入锁。之所以这么说是因为在Java中,锁主要就是指重入锁。 java中的锁分为两大类:一种是synchronized内置锁,另一种是显式的Lock锁。在Java中,Lock接口的主要实现是重入锁ReentrantLock,而内置锁也是可重入锁。

这两种锁的简单比较如下:

synchronized内置锁和ReentrantLock都是可重入锁。 synchronized就不是可中断锁,而Lock是可中断锁。  synchronized 和Lock都是不公平锁。 Lock可以设置为公平锁。

在nts 生产代码中,并没有使用显式Lock,而是大量地使用了synchronized关键字。本文主要包括这两种锁的实现、主要的方法和使用。其中在讲解使用时,会结合在jdk中的使用以及在nts中的使用等方式来讲解。

outline:

1 java 的内置锁(synchronized关键字)的使用

2 ReentrantLock的实现、主要方法和使用

3 Condition的实现、主要方法和使用

4 ReentrantReadWriteLock的实现、主要方法和使用

1 java 的内置锁(synchronized关键字)的使用:

synchronized 关键字有两种用法:synchronized 方法和 synchronized 块。其中synchronized方法又分为静态方法和实例方法两种。synchronized块又可以分为普通对象块,this对象块和class对象块。由于可以针对任意代码块,且可任意指定上锁的对象,故synchronized块的灵活性较高。

当一个线程获取了对应的内置锁,并执行该代码块时,其他线程便只能一直等待获取锁的线程释放锁,而获取锁的线程只会在两种情况下释放锁:

1)获取锁的线程执行完了该代码块,然后释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

1.1 synchronized 方法:

1 //in push.Latch

2 public synchronized void enter(final long timeout) throwsInterruptedException //通过在方法声明中加入 synchronized关键字来声明 synchronized 方法3 {4 num++;5 if (num

13 {14 notifyAll(); // must in a synchronized method15 }16 }

1.2 staticsynchronized 方法

static synchronized方法是对class的类对象加锁,synchronized方法是对class的当前object加锁。

1 //in dca.access.IconStore

2 public static synchronizedIconStore getInstance() // 性能较差的singleton模式3 {4 try

5 {6 if (null ==instance)7 {8 instance = newIconStore();9 }10 }11 catch (finalNotesException e)12 {13 XLog.error("Could not create icon store object", e);14 }15

16 returninstance;17 }

1.3 普通对象上的synchronized 块:

1 //nts.util.Events

private static final Map events = new HashMap()

...

2 publicObject clone()3 {4 Events s = null;5

6 try

7 {8 synchronized(events) // on plain object9 {10 s = (Events) super.clone();11 }12 }13 catch (finalCloneNotSupportedException e)14 {15 XLog.error(e);16 }17 returns;18 }

1.4 类对象上的synchronized 块

1 private staticTravelerSocketFactory getInstance() // in util.TravelerSocketFactory2 {3 if (instance == null)4 {5 synchronized (TravelerSocketFactory.class)// double-checking lock6 {7 if (instance == null)8 {9 instance = newTravelerSocketFactory();10 }11 }12 }13 returninstance;14 }

1.5  this 对象上synchronized 块

1 //in push.Latch

2 public boolean start(finalObject obj)3 {4 final long timeStart =System.currentTimeMillis();5 boolean rv = false;6 Barrier b = null;7 synchronized (this) // 当前对象8 {9 if (!latched.containsKey(obj))10 {11 b = new Barrier("Latch:" + name + "_Obj:" + obj, 1);12 latched.put(obj, b);13 rv = true;14 }15 }16 XLog.exiting("name=" + name, "obj=" + obj, "barrier=" + b, "rv=" + rv, ("Elapsed time="

17 + (System.currentTimeMillis() - timeStart) + "ms"));18 returnrv;19 }

1.6 Java内置锁的可重入性

下面这段code可以证明对象关联monitor上的锁是重入锁。如果锁具备可重入性,则称作为可重入锁。一个线程不能获取其他线程所拥有的锁,但是它可以获取它已经拥有的锁。 允许一个线程多次获取同一个锁,使得重入同步成为可能。考虑以下场景:同步代码(位于同步块或者同步方法中的代码)直接或者间接地调用某个方法,而该方法 同样包含同步代码(上述两组代码使用同样的锁)。如果没有重入同步,同步代码将不得不使用额外的预警机制来避免一个线程因为自己阻塞自己而死锁。可重入性 在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

1 classTest {2 public static voidmain(String[] args) {3 Test t = newTest();4 synchronized(t) {5 synchronized(t) {6 System.out.println("made it!");7 }8 }9 }10 }

程序的输出结果为:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 made it!

View Code

在JICP中举了一个例子,是两个synchronized方法。其中一个调用了另外一个。如果不是可重入锁,则会死锁。

2 jdk中的显式Lock

java.util.concurrent包是java5的一个重大改进,java.util.concurrent包提供了多种线程间同步和通信的机制,比如Executors, Queues, Timing, Synchronizers和Concurrent Collections等。其中Synchronizers包含了五种:Semaphore信号量,CounDownLatch倒计时锁存器,CyclicBarrier循环栅栏,Phaser和Exchanger。 另外java.util.concurrent包还包含了两个子包:java.util.concurrent.Atomics和java.util.concurrent.Locks。

2.1 Lock接口的方法及使用

Lock接口一共定义了以下6个不同的方法:其中lock()、lockInterruptibly(), TryLock()、和tryLock(long time, TimeUnit unit))是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 用来生成新的Condition对象。(跟Lock用来替代Synchronized类似,Condition用来替代object monitor上的wait()/notify()方法。) 下面分别来介绍这些方法的使用:

2.1.1  void lock()

Lock接口获取锁的方式有4中,首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。(跟synchronized一样,可以使用synchronized代替lock。)

1 //java.util.concurrent.CyclicBarrier

2 public booleanisBroken() {3 final ReentrantLock lock = this.lock; // this.lock is a feild in CyclicBarrier, this is a optimize method which is heavily used by doug lea4 lock.lock();5 try{6 returngeneration.broken;7 } finally{8 lock.unlock();9 }10 }

lock()的不正确使用方法示例:

1 public classTest {2 private ArrayList arrayList = new ArrayList();3 public static voidmain(String[] args) {4 final Test test = newTest();5

6 newThread(){7 public voidrun() {8 test.insert(Thread.currentThread());9 };10 }.start();11

12 newThread(){13 public voidrun() {14 test.insert(Thread.currentThread());15 };16 }.start();17 }18

19 public voidinsert(Thread thread) {20 Lock lock = new ReentrantLock(); //注意这个地方

21 lock.lock();22 try{23 System.out.println(thread.getName()+"得到了锁");24 for(int i=0;i<5;i++) {25 arrayList.add(i);26 }27 } catch(Exception e) {28 //TODO: handle exception

29 }finally{30 System.out.println(thread.getName()+"释放了锁");31 lock.unlock();32 }33 }34 }

2.1.2  boolean tryLock()方法

在当前时间(被调用时)查询锁是否可用。如果不可用则立即返回。

1 //java.util.concurrent.ForkJoinTask

2 static final voidhelpExpungeStaleExceptions() {3 final ReentrantLock lock =exceptionTableLock;4 if(lock.tryLock()) {5 try{6 expungeStaleExceptions();7 } finally{8 lock.unlock();9 }10 }11 }

2.1.3 void lockInterruptibly() throws InterruptedException;

这个方法涉及到一个概念,可中断锁。顾名思义,就是可以响应中断的锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。用synchronized修饰的话,当一个线程处于等待 某个锁的状态,是无法被中断的,只有一直等待下去。Lock可以让等待锁的线程响应中断,而synchronized却不行,使用 synchronized时,等待的线程会一直等待下去,不能够响应中断(在threadA中调用threadB.interrupt();)。

//java.util.concurrent.ArrayBlockingQueue

public void put(E e) throwsInterruptedException {

checkNotNull(e);final ReentrantLock lock = this.lock;

lock.lockInterruptibly(); // can be interrupted heretry{while (count ==items.length)

notFull.await(); // notFull is a Condition of lock.

enqueue(e);

}finally{

lock.unlock();

}

}

2.1.4 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

在jdk和nts生产代码中没有该方法的使用。

2.1.5 Condition newCondition();

用来返回一个Condition接口的对象,Condition对象的使用见下一节。

2.1.6 void unlock();

采用synchronized不需要用户去手动释放 锁,当synchronized方法或者 synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出 现死锁现象。 synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock() 去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

1 public boolean addAll(Collection extends E>c) { // java.util.concurrent.CopyOnWriteArrayList

2 Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?

3 ((CopyOnWriteArrayList>)c).getArray() : c.toArray();

4 if (cs.length == 0)

5 return false;

6 final ReentrantLock lock = this.lock;

7 lock.lock();

8 try{

9 Object[] elements =getArray();

10 int len =elements.length;

11 if (len == 0 && cs.getClass() == Object[].class)

12 setArray(cs);

13 else{

14 Object[] newElements = Arrays.copyOf(elements, len +cs.length);

15 System.arraycopy(cs, 0, newElements, len, cs.length);

16 setArray(newElements);

17 }

18 return true;

19 } finally{20 lock.unlock();21 }22 }

2.2 ReentrantLock的实现:

ReentrantLock内部有一个Sync内部类,它是抽象类AQS(abstractQueuedSychronizer)的子类,它又有两个子类NonFairSync和FairSync。

ReentrantLock的上述方法都是通过Sync内部类来实现的。

newCondition():可见ReentrantLock的实现基本完全基于AQS,ReentrantLock返回的Condition对象,是它的AQS内部类的内部类ConditionObject的实例(在下一节会详述):

1 publicCondition newCondition() {

2 returnsync.newCondition();

3 }

lock(): 包裹了sync内部类的lock方法。

1 public voidlock() {

2 sync.lock();

3 }

tryLock():

1 public boolean tryLock(longtimeout, TimeUnit unit)

2 throwsInterruptedException {

3 return sync.tryAcquireNanos(1, unit.toNanos(timeout));

4 }

lockInterruptibly():

1 public void lockInterruptibly() throwsInterruptedException {

2 sync.acquireInterruptibly(1);

3 }

unLock():

1 public voidunlock() {

2 sync.release(1);

3 }

下面再来看一下ReentrantLock如何实现公平锁和非公平锁(公平锁:当持有该锁的线程释放掉公平锁的时候,等待该锁的其他线程获得该锁的机会均等,不存在某个线程多次获得该锁,而其他某个线程一直处在饥饿状态的情况。)

FairSync:

1 protected final boolean tryAcquire(intacquires) {

2 final Thread current =Thread.currentThread();

3 int c =getState();

4 if (c == 0) {

5 if (!hasQueuedPredecessors() && // 如果处于等待队列的最前面

6 compareAndSetState(0, acquires)) {

7 setExclusiveOwnerThread(current);

8 return true;

9 }

10 }

11 else if (current ==getExclusiveOwnerThread()) {

12 int nextc = c +acquires;

13 if (nextc < 0)

14 throw new Error("Maximum lock count exceeded");

15 setState(nextc);

16 return true;

17 }

18 return false;

19 }

20 }

NonFairSync():

1 final boolean nonfairTryAcquire(intacquires) {

2 final Thread current =Thread.currentThread();

3 int c =getState();

4 if (c == 0) {

5 if (compareAndSetState(0, acquires)) { // 直接开抢,谁抢着谁执行

6 setExclusiveOwnerThread(current);

7 return true;

8 }

9 }

10 else if (current ==getExclusiveOwnerThread()) {

11 int nextc = c +acquires;

12 if (nextc < 0) //overflow

13 throw new Error("Maximum lock count exceeded");

14 setState(nextc);

15 return true;

16 }

17 return false;

18 }

公平锁和非公平锁可以通过ReentrantLock的构造函数来实现,默认构造函数用来构造NonFairLock, 而带参数的public ReentrantLock(boolean fair) 方法可以选择构造NonFairLock还是FairLock。

1 publicReentrantLock() {

2 sync = newNonfairSync();

3 }

带参数构造函数:

1 public ReentrantLock(booleanfair) {

2 sync = fair ? new FairSync() : newNonfairSync();

3 }

ReentrantLock 还提供了几个查询方法:getHoldCount,isHoldByCurrentThread,isLocked, isFair, getOwner, hasQueuedThreads, getQueueLength, 等等,不再赘述。

3Condition的实现、主要方法和使用

Lock与Condition的组合,可以用来替代Synchronized&wait/notify的组合。

3.1 实现

如前所述,Condition的实现也离不开AQS。

返回函数:

1 publicCondition newCondition() { // in java.util.concurrent.ReentrantLock

2 returnsync.newCondition();

3 }

sync是什么呢? 在 ReentrantLock类中定义了一个抽象class Sync(抽象类AQS的子类),它又有两个子类FairSync和NonFairSync。上面代码中的sync就是NonFairSync的对象。

1 abstract static class Sync extendsAbstractQueuedSynchronizer {

2 ...

3 finalConditionObject newCondition() {

4 returnnewConditionObject();

5 }

6 ...

7 }

在java中实现了 Conditon接口的类只有两 个:AbstractQueuedSynchronizer.ConditionObject和 AbstractLongQueuedSynchronized.ConditionObject。

3.2 方法

Condition接口通过提供下列方法,提供了与Object.notify()/Object.notifyAll()类似的功能:

1 //可以被中断,无期限等待。

2 void await() throwsInterruptedException;3 //无期限等待,不可中断。

4 voidawaitUninterruptibly();5 //等待特定时间,超时则返回;可中断。返回值是返回时距离超时所剩时间。

6 long awaitNanos(long nanosTimeout) throwsInterruptedException;7 //等待特定时间,超时则返回;可中断。超时返回,返回值为false。

8 boolean await(long time, TimeUnit unit) throwsInterruptedException;9 //等待到特定时间点,超时则返回;可中断。超时返回,返回值为false。

10 boolean awaitUntil(Date deadline) throwsInterruptedException;11 voidsignal();12 void signalAll();

3.3 CyclicBarrier中的使用

下面以java.util.concurrent包下的CyclicBarrier和BlockingQueue(接口)为例,来看一下这些方法的使用,首先看一下CyclicBarrier中的:

NewCondition():

1 //in CyclicBarrier

2 /**The lock for guarding barrier entry */

3 private final ReentrantLock lock = newReentrantLock();

4 /**Condition to wait on until tripped */

5 private final Condition trip = lock.newCondition();

signalAll():

1 private voidnextGeneration() {3 //signal completion of last generation

4 trip.signalAll();5 //set up next generation

6 count =parties;7 generation = newGeneration();8 }

await()&awaitNanos(long):

1 private int dowait(boolean timed, longnanos) // CyclicBarrier2 throwsInterruptedException, BrokenBarrierException,3 TimeoutException {4 final ReentrantLock lock = this.lock;5 lock.lock();6 try{7 final Generation g =generation;8

9 if(g.broken)10 throw newBrokenBarrierException();11

12 if(Thread.interrupted()) {13 breakBarrier();14 throw newInterruptedException();15 }16

17 int index = --count; // count is how many thread will be barriered18 if (index == 0) { //tripped

19 boolean ranAction = false;20 try{21 final Runnable command =barrierCommand;22 if (command != null)23 command.run();24 ranAction = true;25 nextGeneration();26 return 0;27 } finally{28 if (!ranAction)29 breakBarrier();30 }31 }32

33 //loop until tripped, broken, interrupted, or timed out

34 for(;;) {35 try{36 if (!timed)37 trip.await();38 else if (nanos > 0L)39 nanos =trip.awaitNanos(nanos);40 } catch(InterruptedException ie) {41 if (g == generation && !g.broken) {42 breakBarrier();43 throwie;44 } else{45 //We're about to finish waiting even if we had not46 //been interrupted, so this interrupt is deemed to47 //"belong" to subsequent execution.

48 Thread.currentThread().interrupt();49 }50 }51

52 if(g.broken)53 throw newBrokenBarrierException();54

55 if (g !=generation)56 returnindex;57

58 if (timed && nanos <= 0L) {59 breakBarrier();60 throw newTimeoutException();61 }62 }63 } finally{64 lock.unlock();65 }66 }

3.4 ArrayBlockingQueue中的使用:

newCondition():

可以为一个重入锁设置多个Condition对象

1 public ArrayBlockingQueue(int capacity, booleanfair) {2 if (capacity <= 0)3 throw newIllegalArgumentException();4 this.items = newObject[capacity];5 lock = newReentrantLock(fair);6 notEmpty =lock.newCondition();7 notFull =lock.newCondition();8 }

signal():

1 public void put(E e) throwsInterruptedException {2 checkNotNull(e);3 final ReentrantLock lock = this.lock;4 lock.lockInterruptibly();5 try{6 while (count ==items.length)7 notFull.await();8 enqueue(e);9 } finally{10 lock.unlock();11 }12 }

3.5 LinkedBlockingQueue中的使用

new:

1 /**Lock held by put, offer, etc*/

2 private final ReentrantLock putLock = newReentrantLock();3

4 /**Wait queue for waiting puts*/

5 private final Condition notFull = putLock.newCondition();

signal():

1 public void put(E e) throwsInterruptedException {2 if (e == null) throw newNullPointerException();3 //Note: convention in all put/take/etc is to preset local var4 //holding count negative to indicate failure unless set.

5 int c = -1;6 Node node = new Node(e);7 final ReentrantLock putLock = this.putLock;8 final AtomicInteger count = this.count;9 putLock.lockInterruptibly();10 try{11 while (count.get() ==capacity) {12 notFull.await();13 }14 enqueue(node);15 c =count.getAndIncrement();16 if (c + 1

4 读写锁 ReadWriteLock 接口

ReadWriteLock接口只提供两个方法定义,其中readLock()方法返回一个ReentrantLock.ReadLock类的对象作为 readLock,另一个writeLock()方法返回一个ReentrantLock.WriteLock类的对象作为writeLock。

当没有写线程时,readLock可以被多个读线程同时持有(互斥锁:在任一时刻只允许一个线程持有,其他线程将被阻塞。 与之对应的是共享锁(S锁,share)又称读锁。)。而写锁则是互斥的: 一些锁允许对共享资源的并发访问,比如一个readWriteLock中的读锁。

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

4.1 示例:

1 import java.util.concurrent.*;2 importjava.util.Date;3 import java.util.concurrent.locks.*;4 import java.text.*;5

6 public classReentrantReadWriteLockTest {7 private ReadWriteLock readWriteLock = newReentrantReadWriteLock();8 private SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");9 private Lock readLock =readWriteLock.readLock();10 private Lock writeLock =readWriteLock.writeLock();11

12 public voidread() {13 readLock.lock();14 try{15 System.out.println(format.format(new Date()) + "---read---");16 TimeUnit.SECONDS.sleep(3);17 } catch(InterruptedException e) {18 e.printStackTrace();19 } finally{20 readLock.unlock();21 }22 }23

24 public voidwrite() {25 writeLock.lock();26 try{27 System.out.println(format.format(new Date()) + "---write---");28 TimeUnit.SECONDS.sleep(3);29 } catch(InterruptedException e) {30 e.printStackTrace();31 } finally{32 writeLock.unlock();33 }34 }35

36 public static class MyThread extendsThread {37 privateReentrantReadWriteLockTest reentrantReadWriteLockTest;38 privateString methodName;39

40 publicMyThread(ReentrantReadWriteLockTest reentrantReadWriteLockTest, String methodName) {41 super();42 this.reentrantReadWriteLockTest =reentrantReadWriteLockTest;43 this.methodName =methodName;44 }45

46 @Override47 public voidrun() {48 if ("read".equalsIgnoreCase(methodName))49 reentrantReadWriteLockTest.read();50 else

51 reentrantReadWriteLockTest.write();52 }53 }54

55 public static voidmain(String[] args) {56 ReentrantReadWriteLockTest test = newReentrantReadWriteLockTest();57 Thread t1 = new MyThread(test, "write"); // replace write with read and check what will happen?58 Thread t2 = new MyThread(test, "read");59 t1.start();60 t2.start();61 }62 }

4.2 使用

读写锁的典型应用场景为对容器的读写。但是,java的容器框架提供了各种各样的免锁或并发容器,比如最简单的并发容器Collections.synchronizedMap()等等。

在免锁容器中,CopyOnWriteArrayList对读操作不会block,对写操作会block;ConcurrentHashMap 类将 Map 的存储空间分为若干块,每块拥有自己的锁,大大减少了多个线程争夺同一个锁的情况:

对读操作不会block 即使是对写操作,我们也不需要对整个Map对象加锁,从而避免在整个对象上block。因此读写锁的应用并不是特别广泛。

下面来看一下jdk中使用ReentantreadWriteLock的例子javax.swing.plaf.nimbus.ImageCache :

1 //Lock for concurrent access to map

2 private ReadWriteLock lock = new ReentrantReadWriteLock();

flush:

为什么flush方法加读锁?

1 public voidflush() {2 lock.readLock().lock();3 try{4 map.clear();5 } finally{6 lock.readLock().unlock();7 }8 }

getImage:

1 public Image getImage(GraphicsConfiguration config, int w, inth, Object... args) {2 lock.readLock().lock();3 try{4 PixelCountSoftReference ref =map.get(hash(config, w, h, args));5 //check reference has not been lost and the key truly matches, in case of false positive hash match

6 if (ref != null &&ref.equals(config,w, h, args)) {7 returnref.get();8 } else{9 return null;10 }11 } finally{12 lock.readLock().unlock();13 }14 }

setImage:

1 public boolean setImage(Image image, GraphicsConfiguration config, int w, inth, Object... args) {2 if (!isImageCachable(w, h)) return false;3 int hash =hash(config, w, h, args);4 lock.writeLock().lock();5 try{6 //...

7 } finally{8 lock.writeLock().unlock();9 }10 }

问题: 与ConcurrentHashMap相比,读写锁的优势在哪里?

4.3 ReentrantReadWireteLock的实现:

内部同样是定义了NonFair和Fair的synchronizer,同样是继承自AQS。

5 进一步问题:

自旋锁:

基于他这种原理,等待的时候,并不释放cpu时间片,相比synchronized  wait()操作,减小了释放,重新获取的消耗。 该自旋锁适用于,当前线程竞争不强烈的时候使用。

进一步解释synchronized和显式Lock的区别:

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。 ??

6 参考文献:

http://www.cnblogs.com/dolphin0520/p/3923167.html

http://blog.csdn.net/HEYUTAO007/article/details/6422412

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值