Java最新吃透这JAVA并发十二核心,面试官都得对你刮目相看,zookeeper面试服务注册

总结

阿里伤透我心,疯狂复习刷题,终于喜提offer 哈哈~好啦,不闲扯了

image

1、JAVA面试核心知识整理(PDF):包含JVMJAVA集合JAVA多线程并发,JAVA基础,Spring原理微服务,Netty与RPC,网络,日志,ZookeeperKafkaRabbitMQ,Hbase,MongoDB,Cassandra,设计模式负载均衡数据库一致性哈希JAVA算法数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算共30个章节。

image

2、Redis学习笔记及学习思维脑图

image

3、数据面试必备20题+数据库性能优化的21个最佳实践

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

同时人们提出了内存屏障happen-beforeaf-if-serial这三种概念来保证系统的可见性原子性有序性

4.2 内存屏障

内存屏障是一种CPU指令,用于控制特定条件下的重排序内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。具有如下功能:

  1. 保证特定操作的执行顺序。
  1. 影响某些数据(或者是某条指令的执行结果)的内存可见性。

在 volatile 中就用到了内存屏障,volatile 部分已详细讲述。

4.3 happen-before

因为有指令重排的存在会导致难以理解CPU内部运行规则,JDK用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。其中CPU的happens-before无需任何同步手段就可以保证的。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。

4.4 af-if-serial

af-if-serial 的含义是不管怎么重排序(编译器和处理器为了提高并行度),单线程环境下程序的执行结果不能被改变且必须正确。该语义使单线程环境下程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

5、volatile

=============================================================================

volatile 关键字的引入可以保证变量的可见性,但是无法保证变量的原子性,比如 a++这样的是无法保证的。这里其实涉及到 JMM 的知识点,Java多线程交互是通过共享内存的方式实现的。当我们读写volatile变量时具有如下规则:

  1. 当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存
  1. 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

volatile就会用到上面说到的内存屏障,目前有四种内存屏障:

  1. StoreStore屏障,保证普通写不和volatile写发生重排序
  1. StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序
  1. LoadLoad屏障,禁止volatile读与后面的普通读重排序
  1. LoadStore屏障,禁止volatile读和后面的普通写重排序

volatile 原理:用volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令,在CPU级别的功能如下:

  1. 将当前处理器缓存行的数据写回到 系统内存。
  1. 这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。

6、单例模式 DCL + volatile

========================================================================================

6.1 标准单例模式

高频考点单例模式:就是将类的构造函数进行private化,然后只留出一个静态的 Instance 函数供外部调用者调用。单例模式一般标准写法是 DCL + volatile

public class SingleDcl {

private volatile static SingleDcl singleDcl; //保证可见性

private SingleDcl(){

}

public static SingleDcl getInstance(){

// 放置进入加锁代码,先判断下是否已经初始化好了

if(singleDcl == null) {

// 类锁 可能会出现 AB线程都在这卡着,A获得锁,B等待获得锁。

synchronized (SingleDcl.class) {

if(singleDcl == null) {

// 如果A线程初始化好了,然后通过vloatile 将变量复杂给住线程。

// 如果此时没有singleDel === null,判断 B进程 进来后还会再次执行 new 语句

singleDcl = new SingleDcl();

}

}

}

return singleDcl;

}

}

6.2 为什么用Volatile修饰

不用Volatile则代码运行时可能存在指令重排,会导致线程一在运行时执行顺序是 1–>2–> 4 就赋值给instance变量了,然后接下来再执行构造方法初始化。问题是如果构造方法初始化执行没完成前 线程二进入发现instance != null,直接给线程二个半成品,加入volatile后底层会使用内存屏障强制按照你以为的执行。

单例模式几乎是面试必考点,,一般有如下特性:

懒汉式:在需要用到对象时才实例化对象,正确的实现方式是 Double Check + Lock + volatile,解决了并发安全和性能低下问题,对内存要求非常高,那么使用懒汉式写法。

饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题。

枚举式:Effective Java 这本书也列举了使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

7、线程池

========================================================================

7.1 五分钟了解线程池

老王是个深耕在帝都的一线码农,辛苦一年挣了点钱,想把钱存储到银行卡里,拿钱去银行办理遇到了如下的遭遇

  1. 老王银行门口取号后发现有柜台营业ing  但是没人办理业务就直接办理了。

  2. 老王取号后发现柜台上都有人在办理,等待席有空地,去坐着等办理去了。

  3. 老王取号后发现柜台都有人办理,等待席也人坐满了,这个时候银行经理看到老王是老实人本着关爱老实人的态度,新开一个临时窗口给他办理了。

  4. 老王取号后发现柜台都满了,等待座位席也满了,临时窗口也人满了。这个时候银行经理给出了若干解决策略

  1. 直接告知人太多不给你办理了。
  1. 采用冷暴力模式,也不给不办理也不让他走。
  1. 经理让老王取尝试跟座位席中最前面的人聊一聊看是否可以加塞,可以就办理,不可以还是被踢走。
  1. 经理直接跟老王说谁让你来的你找谁去我这办理不了。

上面的这个流程几乎就跟JDK线程池的大致流程类似,其中7大参数:

  1. 营业中的3个窗口对应核心线程池数:corePoolSize
  1. 银行总的营业窗口数对应:maximumPoolSize
  1. 打开的临时窗口在多少时间内无人办理则关闭对应:keepAliveTime
  1. 临时窗口存货时间单位:TimeUnit
  1. 银行里的等待座椅就是等待队列:BlockingQueue
  1. threadFactory 该参数在JDK中是 线程工厂,用来创建线程对象,一般不会动。
  1. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,一般有四大拒绝策略

  1. ThreadPoolExecutor.AbortPolicy :丢弃任务,并抛出 RejectedExecutionException 异常。
  1. ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
  1. ThreadPoolExecutor.DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。
  1. ThreadPoolExecutor.DiscardPolicy:丢弃任务,也不抛出异常。

7.2 正确创建方式

使用Executors创建线程池可能会导致OOM。原因在于线程池中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue

  1. ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。
  1. LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,极易容易导致线程池OOM。

正确创建线程池的方式就是自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,

60L, TimeUnit.SECONDS,

new ArrayBlockingQueue(10));

7.3 常见线程池

罗列几种常见的 线程池创建 方式。

  1. Executors.newFixedThreadPool

定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。 使用的无界的等待队列是LinkedBlockingQueue。使用时候小心堵满等待队列。

  1. Executors.newSingleThreadExecutor

创建单个线程数的线程池,它可以保证先进先出的执行顺序

  1. Executors.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  1. Executors.newScheduledThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行

  1. ThreadPoolExecutor

最原始跟常见的创建线程池的方式,它包含了 7 个参数、4种拒绝策略 可用。

7.4 线程池核心点

线程池 在工作中常用,面试也是必考点。关于线程池的细节跟使用在以前举例过一个 银行排队 办业务的例子了。线程池一般主要也无非就是下面几个考点了:

  1. 为什么用线程池。
  1. 线程池的作用。
  1. 7大重要参数
  1. 4大拒绝策略
  1. 常见线程池任务队列,如何理解有界跟无界。
  1. 常用的线程池模版。
  1. 如何分配线程池个数,IO密集型还是 CPU密集型
  1. 设定一个线程池优先级队列,Runable类要实现可对比功能,任务队列使用优先级队列。

8、ThreadLocal

================================================================================

ThreadLocal 可以简单理解为线程本地变量,相比于  synchronized 是用空间来换时间的思想。他会在每个线程都创建一个副本,在线程之间通过访问内部副本变量的形式做到了线程之间互相隔离。这里用到了 弱引用 知识点:

如果一个对象只具有弱引用,那么GC回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存

8.1 核心点

每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,那么当某个ThreadLocal对象不再使用(没有其它地方再引用)时,每个已经关联了此ThreadLocal的线程怎么在其内部的ThreadLocalMap里做清除此资源呢?JDK中的ThreadLocalMap没有继承java.util.Map类,而是自己实现了一套专门用来定时清理无效资源的字典结构。其内部存储实体结构Entry<ThreadLocal, T>继承自java.lan.ref.WeakReference,这样当ThreadLocal不再被引用时,因为弱引用机制原因,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,即其线程内部的ThreadLocalMap会释放其对ThreadLocal的引用从而让jvm回收ThreadLocal对象。这里是重点强调下,回收的是Key 也就是ThreadLocal对象,而非整个Entry,所以线程变量中的值T对象还是在内存中存在的,所以内存泄漏的问题还没有完全解决。

接着分析底层代码会发现在调用ThreadLocal.get() 或者 ThreadLocal.set() 都会 定期回收无效的Entry 操作。

9、CAS

========================================================================

Compare And Swap:比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

V:变量内存地址

A:旧的预期值

B:准备设置的新值

当执行 CAS 指令时,只有当 V 对应的值等于 A 时才会用 B 去更新V的值,否则就不会执行更新操作。CAS 可能会带来ABA问题循环开销过大问题、一个共享变量原子性操作的局限性。如何解决以前写过,在此不再重复。

10、Synchronized

==================================================================================

10.1 Synchronized 讲解

Synchronized 是 JDK自带的线程安全关键字,该关键字可以修饰实例方法静态方法代码块三部分。该关键字可以保证互斥性可见性有序性(不解决重排)但保证有序性

Syn 的底层其实是C++代码写的,JDK6前是重量级锁,调用的时候涉及到用户态跟内核态的切换,挺耗时的。JDK6之前 Doug Lea写出了JUC包,可以方便的让用于在用户态实现锁的使用,Syn的开发者被激发了斗志所以在JDK6后对Syn进行了各种性能升级。

10.2 Synchronized 底层

Syn里涉及到了 对象头 包含对象头、填充数据、实例变量。这里可以看一个美团面试题:

问题一:new Object()占多少字节

  1. markword 8字节 + classpointer 4字节(默认用calssPointer压缩) + padding 4字节  = 16字节
  1. 如果没开启classpointer压缩:markword 8字节 + classpointer 8字节 = 16字节

问题二:User (int id,String name) User u = new User(1,“李四”)

markword 8字节 + 开启classPointer压缩后classpointer 4字节 + instance data int 4字节 + 开启普通对象指针压缩后String4字节 + padding 4  = 24字节

10.3 Synchronized  锁升级

synchronized 锁在JDK6以后有四种状态,无锁偏向锁轻量级锁重量级锁。这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态。大致升级过程如下:

锁对比

| 锁状态 | 优点 | 缺点 | 适用场景 |

| — | — | — | — |

| 偏向锁 | 加锁解锁无需额外消耗,跟非同步方法时间相差纳秒级别 | 如果竞争线程多,会带来额外的锁撤销的消耗 | 基本没有其他线程竞争的同步场景 |

| 轻量级锁 | 竞争的线程不会阻塞而是在自旋,可提高程序响应速度 | 如果一直无法获得会自旋消耗CPU | 少量线程竞争,持有锁时间不长,追求响应速度 |

| 重量级锁 | 线程竞争不会导致CPU自旋跟消耗CPU资源 | 线程阻塞,响应时间长 | 很多线程竞争锁,切锁持有时间长,追求吞吐量时候 |

10.4 Synchronized 无法禁止指令重排,却能保证有序性

指令重排是程序运行时 解释器 跟 CPU 自带的加速手段,可能导致语句执行顺序跟预想不一样情况,但是无论如何重排 也必须遵循 as-if-serial

避免重排的最简单方法就是禁止处理器优化跟指令重排,比如 volatile 中用内存屏障实现,syn是关键字级别的排他且可重入锁,当某个线程执行到一段被syn修饰的代码之前,会先进行加锁,执行完之后再进行解锁。

当某段代码被syn加锁后跟解锁前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。所以代码在执行的时候是单线程执行的,这就满足了as-if-serial语义,正是因为有了as-if-serial语义保证,单线程的有序性就天然存在了。

10.5 wait 虚假唤醒

虚假唤醒定义:

  1. 当一个条件满足时,很多线程都被唤醒了,但只有其中部分是有用的唤醒,其它的唤醒是不对的,
  1. 比如说买卖货物,如果商品本来没有货物,所有消费者线程都在wait状态卡顿呢。这时突然生产者进了一件商品,唤醒了所有挂起的消费者。可能导致所有的消费者都继续执行wait下面的代码,出现错误调用。

虚假唤醒原因:

因为 if 只会执行一次,执行完会接着向下执行 if 下面的。而 while 不会,直到条件满足才会向下执行 while下面的。

虚假唤醒 解决办法:

在调用 wait 的时候要用 while 不能用 if

10.6  notify()底层

  1. 为何waitnotify必须要加synchronized

synchronized 代码块通过 javap 生成的字节码中包含monitorenter  和  monitorexit  指令线程,执行 monitorenter 指令可以获取对象的 monitor,而 wait 方法通过调用 native 方法 wait(0) 实现,该注释说:The current thread must own this object’s monitor

  1. notify 执行后立马唤醒线程吗?

notify/notifyAll 调用时并不会真正释放对象锁,只是把等待中的线程唤醒然后放入到对象的锁池中,但是锁池中的所有线程都不会立马运行,只有拥有锁的线程运行完代码块释放锁,别的线程拿到锁才可以运行。

public void test()

{

Object object = new Object();

synchronized (object){

object.notifyAll();

while (true){

// TODO 死循环会导致 无法释放锁。

}

}

}

11、AQS

=========================================================================

11.1 高频考点线程交替打印

目标是实现两个线程交替打印,实现字母在前数字在后。你可以用信号量、Synchronized关键字跟Lock实现,这里用 ReentrantLock简单实现:

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class Main {

private static Lock lock = new ReentrantLock();

private static Condition c1 = lock.newCondition();

private static Condition c2 = lock.newCondition();

private static CountDownLatch count = new CountDownLatch(1);

public static void main(String[] args) {

String c = “ABCDEFGHI”;

char[] ca = c.toCharArray();

String n = “123456789”;

char[] na = n.toCharArray();

Thread t1 = new Thread(() -> {

try {

lock.lock();

count.countDown();

for(char caa : ca) {

c1.signal();

System.out.print(caa);

c2.await();

}

c1.signal();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

});

Thread t2 = new Thread(() -> {

try {

count.await();

lock.lock();

for(char naa : na) {

c2.signal();

System.out.print(naa);

c1.await();

}

c2.signal();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

});

t1.start();

t2.start();

}

}

11.2 AQS底层

上题我们用到了ReentrantLockCondition ,但是它们的底层是如何实现的呢?其实他们是基于AQS 的 同步队列 跟 等待队列 实现的!

11.2.1 AQS 同步队列

学 AQS 前 CAS + 自旋 + LockSupport  + 模板模式 必须会,目的是方便理解源码,感觉比 Synchronized 简单,因为是单纯的 Java 代码。个人理解 AQS 具有如下几个特点:

  1. 在AQS 同步队列中 -1 表示线程在睡眠状态
  1. 当前Node节点线程会把前一个Node.ws = -1。当前节点把前面节点ws设置为-1,你可以理解为:你自己能知道自己睡着了吗?只能是别人看到了发现你睡眠了!
  1. 持有锁的线程永远不在队列中。
  1. 在AQS队列中第二个才是最先排队的线程。
  1. 如果是交替型任务或者单线程任务,即使用了Lock也不会涉及到AQS 队列。
  1. 不到万不得已不要轻易park线程,很耗时的!所以排队的头线程会自旋的尝试几个获取锁。
  1. 并不是说 CAS 一定比SYN好,如果高并发执行时间久 ,用SYN好, 因为SYN底层用了wait() 阻塞后是不消耗CPU资源的。如果锁竞争不激烈说明自旋不严重 此时用CAS。
  1. 在AQS中也要尽可能避免调用CLH队列,因为CLH可能会调用到park,相对来耗时。

ReentrantLock底层

11.2.2 AQS 等待队列

当我们调用 Condition 里的 await 跟 signal 时候底层其实是这样走的。

12、线程思考

==========================================================================

12.1. 变量建议使用栈封闭

所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。方法调用的时候会有一个栈桢,这是一个独立的空间。在这个独立空间创建跟使用则绝对是安全的,但是注意不要返回该变量哦!

12.2. 防止线程饥饿

优先级低的线程总是得不到执行机会,一般要保证资源充足、公平的分配资源、防止持有锁的线程长时间执行。

12.3  开发步骤

多线程编程不要为了用而用,引入多线程后会引入额外的开销。量应用程序性能一般:服务时间、延迟时间、吞吐量、可伸缩性。做应用的时候可以一般按照如下步骤:

  1. 先确保保证程序的正确性跟健壮性,确实达不到性能要求再想如何提速。
  1. 一定要以测试为基准。
  1. 一个程序中串行的部分永远是有的.
  1. 装逼利器:阿姆达尔定律 S=1/(1-a+a/n)

阿姆达尔定律中 a为并行计算部分所占比例,n为并行处理结点个数:

  1. 当1-a=0时,(即没有串行,只有并行)最大加速比s=n;
  1. 当a=0时(即只有串行,没有并行),最小加速比s=1;
  1. 当n无穷大时,极限加速比s→ 1/(1-a),这就是加速比的上限。例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。

12.4 影响性能因素

  1. 缩小锁的范围,能锁方法块尽量不要锁函数

  2. 减少锁的粒度跟锁分段,比如ConcurrentHashMap的实现。

  3. 读多写少时候用读写锁,可提高十倍性能。

  4. 用CAS操作来替换重型锁。

  5. 尽量用JDK自带的常见并发容器,底层已经足够优化了。

13、End

=========================================================================

都看到这了,送你几个高频面试题吧。

  1. synchronizedReentrantLock使用区别跟底层实现以及重入底层原理
  1. 描述下锁的四种状态跟升级过程
  1. CAS是什么?CAS的弊端是什么?
  1. 你对volatile的理解,可见性跟指令重排咋实现的。
  1. 一个对象创建过程是怎么样的。对象在内存中如何分布的,看 JVM 即可。

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

Elasticsearch 24 题 +Memcached + Redis 40题:

Spring 26 题+ 微服务 27题+ Linux 45题:

Java面试题合集:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

你几个高频面试题吧。

  1. synchronizedReentrantLock使用区别跟底层实现以及重入底层原理
  1. 描述下锁的四种状态跟升级过程
  1. CAS是什么?CAS的弊端是什么?
  1. 你对volatile的理解,可见性跟指令重排咋实现的。
  1. 一个对象创建过程是怎么样的。对象在内存中如何分布的,看 JVM 即可。

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

[外链图片转存中…(img-LuUsUeh8-1715428025276)]

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

[外链图片转存中…(img-AuliNOlu-1715428025276)]

Elasticsearch 24 题 +Memcached + Redis 40题:

[外链图片转存中…(img-OBTSIjGG-1715428025276)]

Spring 26 题+ 微服务 27题+ Linux 45题:

[外链图片转存中…(img-CAaODO6b-1715428025277)]

Java面试题合集:

[外链图片转存中…(img-CbPUGQpb-1715428025277)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值