synchronized-锁总结

目录

一、相关知识点

1.1、对象头

1.2、锁相关概念

1.3、查看对象头工具

二、锁流程

2.1、加锁:monitorenter

2.2、释放锁:monitorexit


一、相关知识点

在 JDK1.6 之前,synchronized只有传统的锁机制(重量级锁),因此给开发者留下了synchronized关键字相比于其他同步机制性能不好的印象。 在 JDK1.6 引入了两种新型的锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

1.1、对象头

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data:成员变量等)和对齐填充(Padding)。

这里写图片描述

具体可参考:

java对象结构_朱清震的博客-CSDN博客_对象头结构

对象头(Object Header)包括以下信息:

  • Mark Word:用于存储对象自身的运行时数据, 如一致性哈希码(HashCode:默认为0,调用Object的hashCode()或System.identityHashCode(对象)会生成HashCode存放在对象头中,一旦生成,就不可变,再次调用也是返回相同的HashCode。调用重写的hashCode()方法生成的HashCode不会影响到对象头的HashCode)、GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳等。
  • Klass Pointer:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • Array Length(只有数组对象有):如果是数组对象,则对象头中有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 

32位的操作系统的MarkWord结构如图

1.2、锁相关概念

  • 锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。
  • 锁消除(Lock Elimination):在JIT(即时编译器)运行的时候,发现某段代码加了同步的代码即使去掉同步也不会出现线程安全问题,就不会执行这个同步加锁的操作。锁消除的主要判定源于逃逸分析,即代码用到的堆的数据如果不会被其他线程访问到,则会认为这些数据不会出现线程安全问题。 
  • 偏向锁(Biased Locking):JDK1.6 引入。目的是消除数据在无竞争情况下的同步原语。线程只需使用1条CAS原子指令获取偏向锁,尝试让偏向锁偏向自己,下一次同一个线程如果获取的是偏向自己的偏向锁时,无需任何同步操作,可直接获取进入同步代码块。 
  1. 适用场景:锁长时间由同一个线程获取使用,没有其他线程来获取锁。
  2. JDK1.6开始,默认开启偏向锁。由于Java程序刚启动时竞争较激烈,所以默认偏向锁在程序启动后大概4S才生效。
  • 轻量级锁(LightWeight Locking):JDK1.6 引入。在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠2条CAS原子指令就可以完成锁的获取及释放。
  1. 适用场景:不同线程交替获取使用锁,没有发生竞争。
  2. 轻量级锁使用了栈帧的LockRecord来存储锁对象的MarkWord(无锁状态)。

  • 重量级锁(HeavyWeight Locking):传统意义的锁,利用操作系统底层的同步机制去实现Java中的线程同步,操作系统会堵塞获取不到锁的线程,将线程的状态由用户态切换到内核态。
  1. 适用场景:一个锁被不同线程同时获取使用,发生了竞争。
  2. 通过创建ObjectMonitor对象来存储锁对象的MarkWord(无锁状态)、锁对象的拥有者、堵塞线程等。

  • 适应性自旋(Adaptive Spinning):JDK1.6 引入。为了避免频繁切换线程上下文(挂起、恢复线程的状态切换)而消耗性能,利用自旋(忙循环一段时间)尝试获取锁。自旋时间根据之前锁的自旋时间和线程的状态,动态自适应变化,用以尽可能减少阻塞线程的几率。
  1. 注意点:只有重量级锁才有自旋尝试获取锁,轻量级锁没有自旋尝试获取锁。轻量级锁中的自旋是如果其他线程正在将轻量级锁膨胀(inflating)为重量级锁,则自旋等待锁膨胀完成,而不是自旋尝试获取锁。

锁的升级:偏向锁->轻量级锁->重量级锁。(轻量级锁如果在解锁时没发生竞争,则会恢复为无锁状态,这个不算锁的降级)

锁一般来说只能升级,不能降级,满足一些苛刻条件可降级锁。 

1.3、查看对象头工具

使用官方提供的jol工具可查看对象头信息。

<!--查看对象头工具-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

二、锁流程

利用synchronized关键字进行加锁,编译成class字节码文件时,会在对应代码处生成字节码指令:1个monitorenter和2个monitorexit(2个monitorexit指令是为了保证如果抛出异常也能正常释放锁)。

2.1、加锁:monitorenter

进入同步代码块时,调用monitorenter指令获取锁对象的使用权。

1)、在当前线程的栈帧中,按内存地址从低到高,找到最后一个可用(obj指针为null)的锁记录LockRecord,若没有可用的LockRecord,则创建一个新的LockRecored,将obj指针指向锁对象。(LockRecord可用来轻量级锁存储锁的无锁状态、当作锁重入计数器等) 

2)、根据对象的锁状态进入不同锁类型的加锁流程

a、偏向锁的加锁流程

当锁对象的锁状态为偏向锁(BiasedLocking),即 101 时,

1、若当前偏向锁偏向的是当前线程,即偏向ID(threadId)为当前线程,且锁对象的类klass开启了偏向锁(类有一个MarkWord的原型prototype,主要存储了偏向开关、epoch等),锁对象的epoch和类的epoch相同,则表示此次的锁是已经偏向当前线程,无需其他操作,执行同步代码块内容。

2、若锁对象的类关闭了偏向锁(触发了批量撤销BulkRevoke,类的MarkWord原型prototype修改为不可偏向状态),则用CAS尝试将偏向锁改为无锁状态(001),不管修改是否成功,进入轻量级锁的加锁流程

3、若锁对象的epoch和类的epoch不相同(触发了批量重定向BulkBiased),则不管偏向锁是否偏向当前线程,都需重新获取锁。构造偏向当前线程的MarkWord,用CAS尝试修改偏向锁的MarkWord。

若修改成功,则说明当前线程获取了该偏向锁,则不需要撤销偏向锁,执行同步代码块内容。

若失败,则说明有其他线程也在竞争该偏向锁,其他线程成功获取了偏向锁,则需要进入撤销偏向锁流程

4、若没进入上述步骤1、2、3,则说明当前偏向锁要么是偏向了其他线程,要么是匿名偏向(AnonymouslyBiased)。构造偏向当前线程的MarkWord,用CAS尝试修改偏向锁的MarkWord。

若修改成功,则说明当前偏向锁是匿名偏向,当前线程成功获取了该偏向锁,执行同步代码块内容。

若失败,则说明当前偏向锁已经偏向了其他线程或有其他线程也在竞争该锁,其他线程成功获取了偏向锁,则需要进入撤销偏向锁流程

5、撤销偏向锁流程(源码:Revoke_Rebiased方法)

 1)若偏向锁是匿名偏向(调用了偏向锁的Object的hashCode()(排除重写的hashCode())、System.identityHashCode(锁对象)可能会走到这个流程),将偏向锁撤销为无锁状态(001),然后进入轻量级的加锁流程

 2)判断类是否开启了偏向锁,若没开启,则用CAS尝试将偏向锁改为无锁状态(001),然后进入轻量级的加锁流程

 3)判断epoch是否有效,偏向锁的epoch与类的epoch是否相同。若不同,则用CAS尝试将偏向锁偏向当前线程。

若成功,则说明当前线程成功获取了该偏向锁,不需要撤销偏向锁,执行同步代码块内容。

若失败,则说明当前偏向锁有其他线程也在竞争该锁,其他线程成功获取了偏向锁,则进入批量重偏向与批量撤销的流程

 4)批量重偏向与批量撤销的流程:在update_heuristics方法里,将类的撤销计数器加1,然后判断撤销计数器是否触发了批量重定向或批量撤销,即达到批量重定向或批量撤销设置的阈值。

 4.1)若触发了批量重定向(BulkBiased),利用VmThread进入安全点safepoint,将类的epoch加1,然后遍历所有存活的线程Thread,根据线程栈帧的LockRecord的obj,更新属于该类的偏向锁的epoch为类的epoch(防止处于同步代码块的锁被重定向为其他线程,破坏了同步互斥性),然后调用Revoke_biase方法撤销或重定向当前偏向锁。

若当前偏向锁没处于加锁状态,则将偏向锁重偏向当前线程,退出安全点后,执行同步代码块内容

若当前偏向锁处于加锁状态,则将偏向锁撤销为轻量级锁,退出安全点后,进入轻量级的加锁流程

 4.2)若触发了批量撤销(BulkRevoke),利用VmThread进入安全点safepoint,遍历所有存活的线程Thread,根据线程栈帧的LockRecord的obj,调用Revoke_biase方法撤销所有属于该类的偏向锁

若当前偏向锁没处于加锁状态,则撤销为无锁状态。退出安全点后,进入轻量级的加锁流程

若当前偏向锁处于加锁状态,则将偏向锁撤销为轻量级锁。退出安全点后,进入轻量级的加锁流程

 4.3)若是没触发批量重定向和批量撤销,则只撤销当前偏向锁。

若当前偏向锁偏向当前线程(当前线程在同步代码中调用hashCode方法可能会走到这里),直接调用Revoke_biase方法撤销偏向锁为轻量级锁,然后进入轻量级的加锁流程

若当前偏向锁不是偏向当前线程,则利用VmThread进入安全点safepoint,调用Revoke_biase方法撤销当前偏向锁。

若偏向锁没处于加锁状态,则撤销为无锁状态,退出安全点后,进入轻量级的加锁流程

若偏向锁处于加锁状态,则将偏向锁撤销为轻量级锁,退出安全点后,进入轻量级的加锁流程

Revoke_biase方法

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
  markOop mark = obj->mark();
  // 如果偏向锁没有开启偏向模式,锁状态不是 101 ,则直接返回NOT_BIASED
  if (!mark->has_bias_pattern()) {
    ...
    return BiasedLocking::NOT_BIASED;
  }

  uint age = mark->age();
  // 构建两个mark word,一个是匿名偏向模式(101),一个是无锁模式(001)
  markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
  markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);

  ...

  JavaThread* biased_thread = mark->biased_locker();
  if (biased_thread == NULL) {
     // 匿名偏向。当调用锁对象的hashcode()方法可能会导致走到这个逻辑
     // 如果不允许重偏向,则将对象的mark word设置为无锁模式
    if (!allow_rebias) {
      obj->set_mark(unbiased_prototype);
    }
    ...
    return BiasedLocking::BIAS_REVOKED;
  }

  // code 1:判断偏向线程是否还存活
  bool thread_is_alive = false;
  // 如果当前线程就是偏向线程 
  if (requesting_thread == biased_thread) {
    thread_is_alive = true;
  } else {
     // 遍历当前jvm的所有存活线程集合,如果能找到,则说明偏向的线程还存活
    for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
      if (cur_thread == biased_thread) {
        thread_is_alive = true;
        break;
      }
    }
  }
  // 如果偏向的线程已经不存活了
  if (!thread_is_alive) {
    // 允许重偏向则将对象mark word设置为匿名偏向状态,否则设置为无锁状态
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      obj->set_mark(unbiased_prototype);
    }
    ...
    return BiasedLocking::BIAS_REVOKED;
  }

  // 偏向线程还存活则遍历线程栈中所有的Lock Record
  GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
  BasicLock* highest_lock = NULL;
  for (int i = 0; i < cached_monitor_info->length(); i++) {
    MonitorInfo* mon_info = cached_monitor_info->at(i);
    // 如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码
    if (mon_info->owner() == obj) {
      ...
      // 需要升级为轻量级锁,直接修改偏向线程栈中的Lock Record。为了处理锁重入的case,在这里将Lock Record的Displaced Mark Word设置为null,第一个Lock Record会在下面的代码中再处理
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);
    } else {
      ...
    }
  }
  if (highest_lock != NULL) {
    // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    ...
  } else {
    // 走到这里说明偏向线程已经不在同步块中了
    ...
    if (allow_rebias) {
       //设置为匿名偏向状态
      obj->set_mark(biased_prototype);
    } else {
      // 将mark word设置为无锁状态
      obj->set_mark(unbiased_prototype);
    }
  }

  return BiasedLocking::BIAS_REVOKED;
}

VM Thread:在JVM中有个专门源源不断的从VMOperationQueue队列中取出请求(比如GC请求)的VM Thread。

对于需要安全点safepoint的操作(VM_Operationevaluate_at_safepoint返回true),必须要等到所有的Java线程进入到safepoint才开始执行。安全点safepoint这个时间点,没有任何Java线程正在运行。例如GC请求需要安全点,保证对象的引用链安全有效。

注意点

  1. 当锁对象为匿名偏向、或已偏向,但未处于加锁状态时,调用Object的hashCode()或System.identityHashCode()方法时,锁对象会被撤销为无锁状态(001)。
  2. 当锁对象为偏向锁,且处于加锁状态,或为轻量级锁,当前线程或其他线程调用了Object的hashCode()或System.identityHashCode()方法时,锁对象会升级为重量级锁(10)。
  3. 类对象为偏向锁,当使用该类的实例对象进行加锁时,类对象会撤销为无锁状态(001)。
  4. 当调用锁对象的waitnotify方法时,如果当前锁的状态是偏向锁或轻量级锁,则会先膨胀成重量级锁。

b、轻量级锁的加锁流程

当锁对象的锁状态为不可偏向,即锁状态不是 101 时,或者偏向锁发生撤销时,

1、在slow_enter方法先判断锁对象的锁状态,若为无锁状态(001),复制锁对象的MarkWord到当前线程的LockRecord的DisplacedMarkWork中,然后用CAS尝试将锁对象的MarkWord指向当前线程的LockRecord。

若修改成功,则说明当前线程成功获取锁对象,执行同步代码块内容

若修改失败,则说明有其他线程也在竞争该锁,调用inflate方法膨胀该锁,得到ObjectMonitor重量级锁后,进入重量级锁的加锁流程

2、若是当前锁状态为轻量级锁(00),则判断当前锁是否是重入锁

若是重入锁,则将LockRecord的DisplacedMarkWork设置为null,充当锁重入计数,执行同步代码块内容

若不是重入锁,则说明当前锁对象正在被其他程序使用,发生竞争,调用inflate方法膨胀该锁,得到ObjectMonitor重量级锁后,进入重量级锁的加锁流程

c、重量级锁的加锁流程

当锁状态为重量级锁(10),或当不同线程竞争同一个锁对象时,需要将锁膨胀为重量级锁,

1、inflate方法中,根据锁的状态判断是否已经膨胀完成。

  • 重量级锁(inflate):表示膨胀完成,返回ObjectMonitor重量级锁。
  • 轻量级锁(LightWeight Locking):创建ObjectMonitor对象,将owner指向拥有轻量级锁的线程的LockRecord,并用CAS尝试将锁对象MarkWord设置为inflating。若成功,则继续初始化Monitor对象,最后修改锁状态为重量级锁,返回ObjectMonitor重量级锁。若失败,则说明其他线程正在将轻量级锁膨胀为重量级锁,销毁刚刚创建的ObjectMonitor重量级锁,然后继续循环等待膨胀完成。
  • 膨胀中(inflating):表示其他线程正在将轻量级锁膨胀为重量级锁中,当前线程自旋等待(不会一直忙循环等待,内部会调用操作系统的park、yield等方法,防止一直占用CPU资源),等膨胀完成后,退出自旋循环,返回膨胀完成的ObjectMonitor重量级锁。
  • 无锁状态(Normal):创建ObjectMonitor对象,并用CAS尝试将锁对象MarkWord设置为inflate。若成功,则返回ObjectMonitor重量级锁。若失败,则说明其他线程已经将锁膨胀为重量级锁,销毁ObjectMonitor重量级锁,然后继续循环,返回膨胀完成的ObjectMonitor重量级锁。

2、调用ObjectMonitor对象的enter方法获取重量级锁。

 2.1)Monitor对象的owner为null,则说明当前锁对象没有其他线程正在使用,用CAS尝试将owner指向当前线程。

若成功,则说明当前线程成功获取锁,执行同步代码块内容。

若失败,继续进行下面步骤。

 2.2)owner指向当前线程,则说明当前线程获取的是重入锁,将Monitor的重入计数器recursions加1,执行同步代码块内容。

 2.3)owner不指向当前线程,则判断是否指向当前线程栈帧的LockRecord。(当前线程如果是之前持有轻量级锁的线程,由轻量级锁膨胀且第一次调用enter方法,那owner是指向的是当前线程栈帧的Lock Record)

若是,则修改owner指向当前线程,将Monitor的重入计数器recursions加1,执行同步代码块内容。

若失败,则继续进行下面步骤。

 2.4)在调用系统的同步操作之前,利用自旋等待尝试获取重量级锁。若是在自旋中,成功获取锁,执行同步代码块内容。

 2.5)若是自旋时间期间没有获取到锁,调用ObjectMonitor对象的EnterI方法继续尝试获取锁或堵塞当前线程。

3、在EnterI方法中,再次自旋尝试获取锁。

若失败,则创建ObjectWaiter对象,封装当前线程的相关信息,然后用CAS尝试插入到Monitor对象的ContentionList(单向链表)的头部中。

若失败,则再一次尝试获取锁,若成功,则返回,执行同步代码块内容。

若尝试失败,继续循环尝试插入到ContentionList的头部中。

若插入成功,则进入到步骤4。

4、若Monitor的responsible为null,用CAS设置responsible为当前线程。(responsible指向的线程调用的是有时间限制的park方法堵塞)

5、再次尝试获取锁,如果失败,利用操作系统的park方法堵塞当前线程。

6、当线程被拥有锁的线程从堵塞状态唤醒时,再次自旋尝试获取锁。若失败,如果Monitor的succ(OnDeck:不为空表示有非堵塞状态的线程正在尝试获取锁)指向当前线程,则设置为null,然后跳到步骤5继续循环。

7、成功获取锁,移除ContentionList或EntryList的对应ObjectWatier,如果responsiblesucc指向当前线程,则设置为null,执行同步代码块内容。

2.2、释放锁:monitorexit

退出同步代码块时,调用monitorexit指令释放锁对象。

注意:这里的释放指的是退出同步代码块。

a、偏向锁的释放锁流程

若当前锁对象的锁状态偏向锁(101),

按内存地址从低到高,找到第一个偏向线程栈帧中obj指向当前锁对象的LockRecord,设置LockRecord的obj为null,退出同步代码块。

b、轻量级锁的释放锁流程

若当前锁对象的锁状态轻量级锁(00),

1、判断LockRecord的DisplacedMarkWord是否为null。

2、若DisplacedMarkWord为null,表示释放的是重入锁,将obj设置为null,退出同步代码块。

3、若DisplacedMarkWord不为null,尝试CAS将锁对象的MarkWord替换成LockRecord的DisplacedMarkWord

若是替换成功,则表示成功释放锁,退出同步代码块。

若是替换失败,则说明该轻量级锁被膨胀为重量级锁,需要进入重量级锁的释放锁流程

c、重量级锁的释放锁流程

若当前锁对象的锁状态重量级锁(10),

1、判断Monitor的owner是否指向当前线程,若不是指向当前线程,则通过判断owner是否指向当前线程栈帧的LockRecord。若是,将owner修改为指向当前线程。

(当前线程是之前持有轻量级锁的线程,由轻量级锁膨胀后还没调用过enter方法,即没重入锁,owner指向的是当前线程栈帧的Lock Record)

2、通过Monitor的recursions判断是否释放的锁是重入锁。若是,则将recursions减-1后,将LockRecord的obj设置为null,退出同步代码块。

3、先将Monitor的owner设置为null,表示完全释放了锁对象(这时如果有其他线程进入同步块则能获得锁),相当于退出同步代码块

4、接着判断Monitor的ContentionListEntryList是否都为空。如果都为空,表示没有线程正在堵塞等待获取锁,跳出循环,然后将当前线程的LockRecord的obj设置为null。

若是ContentionList和EntryList中有个不为空,则判断succ是否为空。若不为空,则表示当前有未堵塞的线程正在尝试获取锁,跳出循环,然后将当前线程的LockRecord的obj设置为null。

5、若是ContentionList和EntryList中有不为空,且succ为空,表示需要当前线程去唤醒一个堵塞的线程赋值给succ,来尝试获取锁。(防止释放了重量级锁,而堵塞的线程还是一直堵塞)

6、当前线程用CAS尝试获取重量级锁去唤醒堵塞线程(唤醒堵塞线程需要当前线程获取该重量级锁)。

若失败,则表示有其他线程获取重量级锁,那就无需当前线程去唤醒堵塞线程来尝试获取锁,跳出循环,然后将当前线程的LockRecord的obj设置为null。

若成功,则表示还没有其他线程去获取重量级锁,需要当前线程去唤醒堵塞线程。

7、根据QMode的不同,会执行不同的唤醒策略

  1. QMode = 2且cxq(contentionList)非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,最后将当前线程的LockRecord的obj设置为null。
  2. QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部。
  3. QMode = 4且cxq非空:把cxq队列插入到EntryList的头部。
  4. QMode = 0(默认):什么都不做,继续往下执行。

8、只有QMode = 2的时候会提前返回,等于0、1、3、4的时候都会继续往下执行。

9、如果EntryList的元素非空,就取首元素出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,最后将当前线程的LockRecord的obj设置为null。

10、如果EntryList的元素为空,且cxq也为空,回到步骤3继续循环。

11、如果cxq不为空,根据QMode的不同,将cxq的所有元素放入到EntryList中。

  1. QMode = 1:将cxq中的元素顺序取反放到EntryList中。
  2. QMode != 1:将cxq的所有元素按照原本顺序放入到EntryList中。

12、再次判断succ是否为null,如果不为空,说明已经有个继承人(非堵塞的线程正在尝试获取锁)了,所以不需要当前线程去唤醒堵塞线程,减少上下文切换的比率,跳到步骤3将owner设置为null。

13、最后唤醒Entrylist的第一个元素,然后立即返回,最后将当前线程的LockRecord的obj设置为null。

参考

https://github.com/farmerjohngit/myblog/issues/12

jdk源码剖析二: 对象内存布局、synchronized终极原理 - 只会一点java - 博客园

synchronized原理和锁优化策略(偏向/轻量级/重量级) - ls_cherish的个人空间 - OSCHINA - 中文开源技术交流社区

对java锁升级,你是否还停留在表面的理解?7000+字和图解带你深入理解锁升级的每个细节-云社区-华为云

java中的各种锁-自旋锁/偏向锁/轻量级锁/重量级锁/乐观锁/悲观锁/分段锁/分布式锁等等 - 知乎

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值