java面试前整理知识点

## <center><b>java面试题总结</b></center>

java并发篇
 -
 - java如何开启一个线程?如何保证线程安全?<br/>
 答:线程是计算机进行任务分配的最小单元,进程是计算机进行资源分配的最小单元。 <br/>
 java中可以直接继承Thread类来实现一个线程类,也可以实现Runable或者callable接口,把对象传给Thread构造函数来实现。
 其中callable是带有返回值的。使用线程池ThreadPoolExecutor也可以启动一个线程,对于runable可以调用execute方法,callable可以调用submit方法。<br/>
 可以加锁保证线程安全。比如synchronized、lock等。
 - 这几种方式有什么区别?通常选择那种方式? <br/>
 答:继承Thread类的线程只能继承此类,不能在继承其他的类,而且该方式也不容易通过多个线程共同操作同一段代码。实现接口的方式相对更灵活,更容易通过多线程去操作同一段代码。<br/>
 Runnable接口没有返回值,Callable接口可以带有返回值。ThreadPoolExecutor是通过线程池来管理线程。一般可以通过线程池的方式来管理线程。
 - 对Thread的sleep,yield,join的理解
 答:sleep(t)方法是让线程睡眠t时间,线程睡眠后cpu在这个时间段内不会调用该线程,但是线程如果持有所资源的话那么是不会释放锁的程序中不推荐使用sleep方法。
 yield方法是让出本次执行机会,线程会进入就绪状态再次争抢cpu,该方法不会释放锁资源。
 join方法是等待目标线程执行完毕,自己在继续执行。join方法是通过synchronized和wait方法实现的。当a线程调用b.join()方法后,a线程会先获取b线程对象的锁,然后进入wait状态,当b线程执行完之后,b线程自动释放掉自己,然后唤醒a。
 - java中线程的状态都有哪些,分别对应哪些情况?
 答:线程的状态有 :
    1、NEW:线程刚刚创建时的状态。
    2、RUNNABLE:当调用线程的start方法后一般会进入RUNNABLE状态
    3、BLOCKED:当线程等待synchronized的锁对象时,就是BLOCKED状态
    4、WAITING:当线程调用了object.wait(),thread.join(),LockSupport.park()时线程进入WAITING状态,当有其他线程低啊用notify,notifyAll,join的线程执行完成,unpark的时候,线程会回到RUNNABLE状态。
    5、TIME_WAITING: 当线程调用了thread.sleep(),object.wait(time),thread.join(time),LockSupport.parkNanos,LockSupport.Until方法时进入WAITING状态,
    6、TERMINATED[ˈtɜːrmɪneɪtɪd]:线程结束的状态
    注意BLOCKED与WAITING状态的区别,BLOCKED状态是线程还没有获取到锁,在锁那等着,WAITINE状态是线程获取到了锁,但是执行了wait方法,把锁让出去,等着其他线程把自己唤醒的状态。
    当程序内出现死锁,或者cpu占用过高时,可以通过jstack命令查看线程的状态来排查问题。
- 线程如何安全性的停止一个线程?
答:interrupt是java为线程提供的一种优雅的停止线程的解决办法。一个线程如何优雅的停止,那就是让线程自己决定我什么时候可以停止。Thread提供一个interrupt(中断)方法,这个方法对于正在运行中的线程只会修改一下中断标志位,至于线程是否去检测并且这个标志位,由线程自己决定。
对于BLOCKED和WAITING状态的线程,JVM会唤醒这些线程,让他立刻去响应这个中断标志位,然后有线程自己决定是否需要停止,并且何时停止。
Thread里有三个方法与中断有关,interrupt(),interrupted,和isInterrupted().其中interrupted会返回线程是否被打断,并且清除标志位,而isInterrupted不会清除。
- ThreadLocal了解吗?原理是啥?
答:在聊ThreadLocal之前,先说两个类Thread,和ThreadLocalMap。在java中Thread类里面有一个ThreadLocal.ThreadLocalMap类型的成员变量,可以把它理解为每个thread线程都有自己的一个map。在线程执行阶段,我们可以往这个map里面存入(key,value)。
ThreadLocal可以帮助我们完成往这个map存取值。local.set(value)方法,首先获取当前线程。然后获取当前线程的ThreadLocalMap对象,然后将local对象作为key,value作为value存入这个map对象里面。对于每个调用了该方法的thread都会有一个key为local对象的应用,value为线程自己存入的value的Entry对象。
当调用local.get的时候,thread会去自己的那个map里面获取key为local的value,这样做到了线程之间的数据隔离。在ThreadLocalMap的实现里面Entry是一个弱引用,他的key为弱引用,value为强引用。当value不用时要及时执行remove将其从map中移除,否则会有内存泄露问题。当然在调用set,rehash,等方法时会将key为空空
的Entry的value置为null,并且把指向这个entry的引用也值为空用的是这个方法expungeStaleEntry().
- java的内存模型
答:java内存模型分为主内存和工作内存,主内存是线程共享的,工作内存是线程独占的,工作内存之间的数据是不可见的。cpu想要操作主存的数据必须经过线程的工作内存,即必须通过 load,read操作然后才能use该值,兑对于想要保存的数据必须通过store,write操作。所以当多个线程同时操作一块共享内存时,
可能会出现线程安全问题。比如线程一对变量a进行了加一操作,如果不及时写会主存并且其他线程不再次从主存读数据时,就会出现数据不一致问题。对于这种情况java提出了happens-before原则,java有8中操作,lock,unlock,read,load,use,assign,store,write.
Happen-Before的规则有以下几条
1、程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
2、管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
3、volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
4、线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动作
5、线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作
6、线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
7、对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
8、传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C
通过读屏障和写屏障可以阻止指令重排序。
- juc包里的东西
答:AtomicInteger,通过cas操作完成线程安全。LongAdder,LongAccumulator,通过分段所+acs高并发的情况下速度更快。
Lock包里的ReentrantLock可重入锁。https://blog.csdn.net/qq_25178353/article/details/107728979?spm=1001.2014.3001.5501
简单来说ReentrantLock是一个可重入的互斥锁,支持公平与非公平两种方式。ReentrantLock有个Sync内部类,Sync继承了AbstractQueuedSynchronizer,在AQS里有几个属性,head,tail,state,waitState,exclusiveOwnerThread,unsafe。其中head,tail维护了一个等待队列,没有抢到锁的都会放入等待队列里
state是线程竞争的锁资源,当线程能通过cas操作将state从0变为1说明获取到了锁,exclusiveOwnerThread用来记录当前拥有锁的线程,unsafe用来完成cas操作。(1.8版本unsafe类是不能获取到的,11版本可以获取到)。有了这些,就可以完成一个加锁流程。说一下非公平锁的加锁流程。线程通过lock.lock()方法进入加锁流程。
1、首先线程会通过compareAndSwap方法cas操作尝试获取锁,如果成功则获得锁,将当前线程记为持有锁线程。否则,执行acquire(1)。
2、acquire(1)里面执行tryAcquire,tyrAcquire执行nonfairTryAcquire(1)尝试获取锁.
  2.1、首先判断state的值是否为0 如果为0 则再次尝试获取锁如果成功则将当前线程记为持有锁线程,返回true。
  2.2、否则判断持有所得线程是否是当前线程,如果是则将state的值+1 返回true。
  2.3、否则返回false
3、如果第二步返回true则说明获取锁成功方法结束,否则进入addWaiter(Node.EXCLUSIVE)
4、创建一个EXCLUSIVE(独占)模式的Node,将Node的prev指向队尾,然后通过cas的操作将队尾的next指针指向自己,如果不成功重复这个操作直到成功为止,然后进入acquireQueued。
5、在acquireQueue里面,是一个死循环,判断node的前一个节点是否为head,如果是head则并且tryAcquire返回true则返回,如果非head或者tryAcquire失败,则进入shouldParkAfterFailedAcquire()
6、shouldParkAfterFailedAcquire()方法里面根据前一个节点的waitStatus的值完成三件事,下面将waitStatus简写为ws
  6.1、如果ws == -1 返回 true
  6.2、如果ws > 0 则节点循环向前找ws<=0的前置节点 返回false
  6.3、如果ws == 0 则通过cas操作将 ws 改为 -1 返回false
7、如果第6步返回true,则执行park操作,直到有其他线程执行unpark(thread),则进入第8步。如果第6步返回false,则重新从第五步执行。
8、回到第五步继续执行<br/>
解锁流程:解锁流程比较简单,因为正常情况下,解锁只有获得锁的那个线程操作,所以不需要cas操作,直接修改status的值就可以
需要注意的是,在unparkSuccess的时候,如果head.next的waitStatus > 0 或者 head.next == null 则需要从队尾开始找waitStatus<=0的node。为啥从队尾开始而不是从队头开始?
因为在入队操作时,先进行了node.prev = tail的操作,然后通过cas将node插入队尾是一个,最后将前tail节点的next指向node,所以当node插入队尾后,前tail节点的next可能还没指向node,所以从头结点开始遍历可能出现head.next == null但是队列不为空的情况,
若从队尾开始找,prev可以保证肯定能指向前一个节点,所以从队尾开始遍历。

 - 介绍一下ThreadPoolExecutor,以及java里已经实现的几种ThreadPoolExecutor。<br/>
 答:ThreadPoolExecutor是java用来管理线程的类,又叫做线程池。在多线程环境中,合理使用该类可以避免频繁的线程创建和销毁,同时可以避免任务过多时创建太多的线程导致程序卡死等问题。<br/>
 ThreadPoolExecutor一共有七个参数:(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) 从左到右的含义依次为
 核心线程数,最大线程数,空闲线程存活时间,存活时间的单位,任务队列,重建线程的工厂,拒绝策略。<br/>
 当一个任务被提交到线程池里时,首先判断正在工作的线程数量是否小于核心线程数,如果小于则使用空闲线程或者创建线程来执行任务,否则将任务加入到等待队列里,如果队列满了,则判断正在工作线程是否大于等于最大线程数,如果小于则创建线程执行任务,否则执行拒绝策略。<br/>
 当线程空闲时间超过keepAliveTime时并且池子中的线程数大于核心线程数时,该线程被终止。<br/>
 java提供四种已经实现的拒绝策略,AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy,从左到右依次为抛出异常,有调用者执行,替换队列存活时间最长的任务,什么都不做。当然用户可以自己定义拒绝策略。<br/>
 常用的方法: 
- volatile和synchronize有什么区别?volatile可不可以保证线程安全?DCL(Double Checked Locking)是否需要加volatile?<br/>
答:volatile有两个作用一是保证数据在线程之间的可见性,二是禁止指令重排序。线程之间可见是指每次线程对该数据都从内存读取,当线程对变量修改时,要刷新主存。
一般在一个线程写多个线程读的场景中使用。synchronized是java提供的锁关键字,他可以保证线程安全。DCL需要加volatile,因为如果不加的话可能会由于指令重排出现其他线程到的单例对象是一个不完整的对象。
- Java线程锁机制是怎样的?偏向锁,轻量级所,重量级锁有什么区别?锁机制是怎样升级的? https://blog.csdn.net/lengxiao1993/article/details/81568130
答:java提供了一个锁关键字synchronized, 其用一个对象当做锁。实际上,synchronized加锁时是在对象头的markword字段的后三位记录锁状态。在jdk1.6版本,synchronized做了优化,有了所升级的概念。<br/>
随着竞争情况的逐渐升级,锁状态也会从偏向锁向自旋锁再向重量级升级。当一个线程去竞争资源时,如果偏向锁标志位为1并且锁状态为01无锁状态时,会判断对象头中存储的线程ID是否等于本线程ID,如果相等这说明该线程已经持有该锁,将计数加一,直接进入同步块。如果不相等则尝试通过cas操作将<br/>
本线程的线程ID写到对象头,如果成功,进入同步代码块,如果失败说明已经有其他线程竞争过该锁,则进行锁升级变为轻量级锁: JVM会在一个全局安全点stoptheworld,找到拥有锁的线程A,在栈中添加LockRecord(LockRecord存储了markWord和被markword地址),并且把markword改为指向LockRecord的地址<br/>
如果线程A已经释放了锁,则将对象锁变为无锁状态。竞争轻量级锁的线程会通过cas自旋的方式将自己的lR地址写入到对象头内,在有限的次数内如果成功则获取到锁,如果不成功则继续锁升级成重量级锁。
- 说一说java指令的执行过程。
答:https://blog.csdn.net/lqj701/article/details/89633061 <br/>
这里主要说一下类的加载过程。首先java命令会创建JVM虚拟机,虚拟机里面使用到的类都是先通过类加载器加载到内存,所以紧接着会创建bootstrapClassLoader(C,C++实现)。然后C,C++调用LauncherHelper的checkAndLoadMain方法来加载main方法所在的类,checkAndLoadMain方法调用了appClassLoader的loadClass方法来加载。
loadClass使用双亲委派的机制来加载类。当类加载完毕后由C++代码调用main方法。
- 什么是双亲委派机制,为什么要使用双亲委派机制,如何自定义类加载器?
答:双亲委派机制是JVM的类加载器加载类时的机制,即当加载一个类时,如果加载当前加载器有父加载器时先调用父加载器,如果父加载器为空时,则先用BootstrapClass加载器加载,当父加载器的加载路径找不到要加载的类时,则自己调用findClass来加载。
使用双亲委派机制是为了防止用户通过自定义jdk已有的类来篡改jdk内已经实现好的类,比如rt.jar里面的类。这样会更加安全。用户可以通过继承ClassLoader类并且实现findClass方法来自定义一个类加载器(需要调用defineClass()方法).自定义加载器的父加载器默认是AppClassLoader。
- 双亲委派机制可以打破吗?
答:可以,因为loadClass方法是public的,所以子类可以重写该类,但是注意,重写时,需要兼容Objec类,因为任何类都是Object类的子类,当加载类时先加载Object类。当用自定义加载器加载自己的类的时候先加载Object类。又因为Object类当前由自定义加载器加载,所以肯定找不到Object类,
需要交给父加载器加载。tomcat就打破了双亲委派机制。
- JVM内存模型是什么?分别有什么作用?
答:hotspot的内存主要有:堆,方法区,栈,本地方法栈和程序计数器。其中堆是用来存放对象的,占最大一部分。方法区是用来存放字节码文件,常量池。栈是每个线程独立拥有的,每个线程会有自己的专属的线程栈,栈内存放局部变量表,操作数栈,方法出口,和动态链接
- 逃逸分析,标量替换
- 对象进入老年代的情况
答:gc年龄超过设置值,对象大于大对象设置值,对象动态年龄判断机制(年龄1+年龄2+。。。+年龄n > S0/2时,则年龄n以上的对象移到老年代),担保机制。调优关键是,尽量让生命周期短的对象通过YongGC回收,减少FullGC次数。
- 垃圾收集算法
答:分代收集理论,复制,标记清除,标记整理,
CMS 三次标记:初始标记-只标记GCRoot直接引用的对象(STW),并发标记(不需要STW),重新标记(STW).CMS会占用CPU资源,系统吞吐量降低,用户体验会更好。CMS产生内存碎片,使用标记-清除算法(可开启整理,但是使用的是SerialOld)。CMS并发标记对于漏标的情况是将新增的引用存入到一个表内,在重新标记时,在扫描一下表内的引用(增量更新)。另一种解决方式是保存
要删除的灰色指向白色的引用,在一个,当老年代有引用指向新生代后,在新生代里会维护一个老指向新的集合,叫做记忆集,这样在进行YongGC时就不需要全局扫描老年代,提高效率。
= 调优工具
jVisualvm,jstack,jmap,jinfo,jstat -gc pid

## spring
- 谈谈Spring IOC的理解?
答:原来,对象是由使用者来控制的,有了spring后,对象的控制权交给spring容器来管理,比如对象的创建,销毁等。一般会配合DI一起使用。DI就是依赖注入,解决对象与对象之间的依赖的,一般使用@Autoware注解。
spring容器就是一个map,里面存放已经创建好的对象(bean),可以通过getBean方法来获取bean对象。整个bean对象从创建到消亡都是由spring容器来管理。spring容器里采用三级缓存来管理不同时期的bean对象。<br/>
1、首先spring容器启动时,会先创建bean工厂即beanFactory(通过obtainFreshBeanFactory()),比如DefaultListableBeanFactory,紧接着设置允许循环依赖标识,将xml配置文件里的beanDefinitions加载到工厂内。
2、创建好工厂后,通过prepareBeanFactory设置其属性,比如添加ApplicationContextAwareProcessor(beanPostProcessor),beanExpressionResolver,注册系统bean等。
3、执行beanFactoryPostProcess,这里是spring预留的扩展点,使用者可以通过实现beanFactoryPostProcess接口来干预工厂创建过程,比如ConfigurationClassPostProcessor,这个类就是用来解析@Configuration,@Import注解的配置类。
4、注册beanPostProcessor, 方便使用人员对bean对象的完成扩展功能 实例化之后,初始化前后。
5、通过反射将上面扫描的所有beanDefinition对象实例化成具体的对象,
6、然后将对象进行初始化,需要完成属性填充,执行 aware,执行beanPostProcessor的前置处理,init-method,afterPropertiesSet方法,beanPostProcessor的后置处理。
7、生成完整的bean对象,放入单例池里面,通过getBean方法可以获取到。
这是我对spring IOC的理解,您觉得有什么问题,可以指点一下我。
- IOC的底层实现
答:1、首先通过createBeanFactory方法创建一个DefaultListableBeanFactory对象
   2、然后扫描包,生成beanDefinitionsMap,
   3、将beanDefinitionsMap里面定义的,非抽象的,单例的,非延迟加载的bean通过反射进行实例化,过程主要有getBean ,doGetBean,getSingleton,createBean,resolveBeanClass,doCreateBean,createBeanInstance,instantiate
   4、实例化后如果允许循环依赖,则将此beanName的ObjectFactory对象提前暴露出来,即在beanFactoryMap里面放入key为beanName,value为ObjectFactory的key:value对。
   5、通过populateBean方法进行属性填充(AutowiredAnnotationBeanPostProcessor),执行initializeBean,aware,postProcessor前置处理器,init-method方法,afterPropertiesSet,postProcessor后置处理器方法
   6、然后将bean对象放入单例池里面,同时将ObjectFactory里提前暴露出来的Factory删除, 
- bean的创建过程
答:1、getBean调用doGetBean,
  2、在doGetBean里面先去依次从三级缓存里面找是否有对象,如果没有则完成对depansOn和beanDefintion的合并得到RootBeanDefinition,然后执行getSingleton给其传入一个singletonFactory对象,
  3、getSingleton方法先把beanName放入一个集合表示beanName正在创建,然后调用singletonFactory的getObejct方法,
  4、getObject里面调用createBean方法
  5、createBean方法里面调用resolveBeanClass方法,将class加载到内存,然后调用InstantiationAwarePostProcessor的applyBeanPostProcessorsBeforeInstantiation方法,完成bean实例化之前的干涉,之后调用doCreateBean
  6、doCreateBean里面调用createBeanInstance
  7、createBeanInstance里面先通过determineConstructorsFromBeanPostProcessors()方法是否能找到构造放上面有@Autowired的构造函数,如果有则通过该构造函数实例化对象,否则走默认构造函数,实例化完成后回到doCreateBean方法里。
  8、继续执行doCreateBean,先MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition方法允许修改befinition,然后在singletonFactory里面放入一个key为beanName,value为ObjectFactory的匿名对象。
  执行populatBean()
  9、populateBean里面,先执行InstantiationAwarePostProcessor的applyBeanPostProcessorsAfterInstantiation方法与第五步的before呼应,然后执行InstantiationAwareBeanPostProcessor类型的BeanPostProcessor的postProcessProperties方法,进行属性填充,这一步会通过AutowiredAnnotationBeanPostProcessor,完成@Autowired,@Value等属性注入。如果有循环依赖也是在这里完成的。执行完回到doCreateBean方法里面
  10、继续执行doCreateBean,执行initializeBean方法,该方法里面会执行aware,postProcessor前置处理,properitesAfterset方法,init-method方法,postProcessor后置处理,如果有代理的话,会在postProcessor完成代理。该方法有返回值,因为如果有代理那么会返回代理对象,与实例化的对象不是一个。
  11、得到最终的单例bean对象,将其放入单例池,并且把其他的几个缓存移除bean。 
- 对spring中aop的理解
答:aop是面向切面的编程,他是面向对象的补充增强,他可以在尽量少的侵入代码的情况下对已有的方法进行增强替换等,比如系统的日志,事务,都可以通过aop来实现。
1、我觉得在说spring的aop的时候,应该从两方面进行说明。一是spring如何实现的aop,如何创建的代理对象。二是,在bean的声明周期中,那一步完成了对bean对象的代理。
aop有几个比较重要的概念,源对象,代理对象,切点,切面和织入,通知。spring中通过Pointcut接口来定义切点,通过这个切点可以找到要进行织入的方法,用advice接口定义通知,即需要增强的方法,spring中只要有三种,beforeAdvice,afterAdvice,和ThrowsAdvice,
spring中将上面两个通过advisor来管理。当上面的东西都定义完了就可以根据源对象的类型来用java的动态代理或者cglib生成代理对象。当调用代理对象的方法时,spring通过之前定义好的拦截器链通过CglibMethodInvocation来执行advice中的通知。我在项目中通过aop的功能完成了对数据查询的加解密。
2、spring在对容器进行初始化时,会完成对单例bean的实例化和初始化,在这个过程中,spring会多次调用beanPostProcessor类型的类,来让使用者可以轻松干预bean的创建,aop就是在bean的生命周期中的InitializeBean中通过AnnotationAwareAspectJAutoProxyCreator
的postProcessorAfterInitialization方法中完成的。具体调用的是ProxyFactory.getProxy()得到代理对象的。
- java动态代理
答:Proxy.newProxyInstance(ClassLoader loader, interfaces,InvocationHandler handler);

- 数据库的四大特性,mysql隔离级别?,postgresSQL事务隔离级别?
答:数据库四大特性 原子性,一致性,隔离性,持久性。隔离级别,Serializable,Repeatable read,Read committed,Read uncommitted
- 怎么理解spring中的事务,spring事务的传播行为,底层实现。
答:spring的事务是针对数据库操作的,在java中用使用jdbc来完成对数据库的操作,数据库驱动,connect对象,statement,resultSet.Spring中对其进行了封装,有了JDBCTemplate,通过DataSource可以创建一个jdbcTemplate。
jdbcTemplate获取Connect,获取statement,执行sql得到resultSet。事务是跟connect绑定的,事务的开启,提交,和回滚都是通过connect来完成的。spring对这个过程进行了抽象比如,DataSourceTransactionObject,这个transaction里面会有connect对象以及一些连接属性。
当开启事务时,实际是通过dataSource创建一个连接然后开启事务,放到一个map集合里面。jdbcTemplate先去map里面获取connect,如果获取不到再去创建connect。这样就将connect与sql分开了,就可以完成更复杂的逻辑处理,比如spring中事务的传播特性。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值