java常见面试题或者知识点(持续更新中)

java 基础

文章目录

静态内部类和非静态内部类的比较

静态内部类只能访问外部类的静态成员和静态方法 非静态内部类不管是静态方法还是非静态方法都可以在非静态内部类中访 问 静态内部类和非静态内部类主要的不同: (1)静态内部类不依赖于外部类实例而被实例化,但非静态内部类需要 在外部类实例化后才可以被实例化 (2)静态内部类不需要持有外部类的引用。但非静态内部类需要持有对 外部类的引用 (3)静态内部类不能访问外部类的非静态成员变量和非静态方法。他只 能访问外部类的静态成员和静态方法,非静态内部类能够访问外部类的静 态和非静态成员和方法

内部类都有哪些?

有四种:静态内部类、非静态内部类、局部内部类、匿名内部类

局部内部类:在外部类的方法中定义的类,其作用的范围是所在的方法内。他不能被 public、private、protected来修饰。他只能访问方法中定义的final类型 的局部变量

public class Test {
    public void method() {
        class Inner {
            // 局部内部类
        }
    }
}

1.4匿名内部类:是一种没有类名的内部类。 需要注意的是:

1、匿名内部类一定是在new的后面,这个匿名内部类必须继承一个父类 或实现一个接口 2、匿名内部类不能有构造函数 3、只能创建匿名内部类的一个实例 4、在Java8之前,如果匿名内部类需要访问外部类的局部变量,则必须用 final修饰外部类的局部变量。在现在Java8已结取消了这个限制

多态的理解与应用

多态概述

  1. 多态是继封装、继承之后,面向对象的第三大特性。

  2. 多态现实意义理解: 现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个 具体的同学张三既是学生也是人,即出现两种形态。 Java作为面向对象的语言,同样可以描述一个事物的多种形态。如 Student类继承了Person类,一个Student的对象便既是Student,又 是Person。

  3. 多态体现为父类引用变量可以指向子类对象。 4.前提条件:必须有子父类关系

  4. 多态是同一个行为具有多个不同表现形式或形态的能力。 多态就是同一个接口,使用不同的实例而执行不同操作

instanceof关键字

用来判断某个对象是否属于某种数据类型

java方法的多态性理解

a. 编译时多态:方法的重载;

b. 运行时多态:JAVA运行时系统根据调用该方法的实例的类型来决定选 择调用哪个方法则被称为运行时多态。(我们平时说得多的事运行时多 态,所以多态主要也是指运行时多态); 上述描述认为重载也是多态的一种表现,不过多态主要指运行时多态。 3.2运行时多态 a. 面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继 承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识 点。

b. 多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可 以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数 调用)

c. 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期 间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。 d. 多态的作用:消除类型之间的耦合关系。

public void show2(){ System.out.println(“C”); } }

多态存在的三个必要条件 一、要有继承; 二、要有重写; 三、父类引用指向子类对象。

java中接口和继承的区别

实际概念区别: 区别

1: 不同的修饰符修饰(interface),(extends)

区别2: 在面向对象编程中可以有多继承!但是只支持接口的多继承,不支持’继承’的 多继承哦 而继承在java中具有单根性,子类只能继承一个父类

区别3: 在接口中只能定义全局常量,和抽象方法 而在继承中可以定义属性方法,变量,常量等… 区别4: 某个接口被类实现时,在类中一定要实现接口中的抽象方法 而继承想调用那个方法就调用那个方法,毫无压力 接口是:对功能的描述 继承是:什么是一种什么 始终记者:你可以有多个干爹(接口),但只能有一个亲爹( 继承)

java无参构造函数的作用

java程序在执行子类的构造函数之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中没有参数的构造方法,因此如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super来调用父类中的特定的构造方法,则编译时会发生错误,因为java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类上加上一个不用做事却没有参数的构造方法。

sychronized,volatile,cas比较下

  • sychronized 是悲观锁,属于抢占式,会引起其他线程阻塞
  • volatile提供多线程共享变量可见性和禁止指令重排序优化
  • cas是基于冲突检测的乐观锁(非阻塞)

java语言是值传递还是引用传递

java中只有值传递,不同的是。

java中concurrentHashMap的并发度是什么

ConcurrentHashMap把实际的map划分成若干部分来实现他的可扩展性和线程安全,这种划分是使用并发度获得的,他是concurrenthashmap类构造函数的可选参数,默认值是16,这样在多线程情况下就能避免征用。在JDK8之后,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现利用了cas算法,同时加入了更多的辅助变量来提高并发度,具体内容还是看源码。

使用final变量时,是引用不能变还是引用的对象不能变

使用final修饰一个变量时,是指引用变量不能变,引用变量所指向的对象的值还是可以变的

对象分配到内存的规则

  1. 对象优先分配在eden区如果Eden区没有足够的空间时,虚拟机执行一次MinorGc

  2. 大对象直接进入老年代,大对象指的是需要大量连续内存空间的对象,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。新生代采用复制算法收集内存。

  3. 长期存活的对象进入老年代,虚拟机为每个对象定义了一个年龄计数器秒如果对象经过了一次MinorGc那么对象会进入Survivor区,之后每经过一次MinorGc那么对像的年龄加1,直到达到fa值的对象进入老年区。

  4. 动态判断对象的年两如果Survivor区中相同年龄的所有对象大小的综合大于SUrvivor空间的一半,年龄大于或等于改年龄的对象可以直接进入老年代。

  5. 空间分配担保。每次进行MinorGc时,JVm会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次FullGC,如果小于检测HandlePromotionFailure设置,如果true则只进行MonorGC,如果false则进行FullGC

    多线程同步和互斥有几种实现方法,都是什么

    线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当他没有得到另一个线程的消息时应等待,知道消息到达时才被唤醒。

    线程互斥是指对于共享的进程共享资源,在各单个线程访问时的排他性,当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他要使用该资源的线程必须等待,知道占用资源者释放该资源,线程互斥可以看成是一种特殊的线程同步。

    线程同步大体分为两类,用户模式和内核模式,内核模式是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态和用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

    用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区,内核模式下的方法有:事件,信号量,互斥量。

    java有自己的内存回收机制为什么还存在内存泄漏。

    首先内存泄露memoryleak是指程序在申请的内存空间无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

    ThreadLocal在实际项目中的意义

    ThreadLocal和其他同步机制相比从另一个角度来解决多线程的并发访问,它为每一个线程维护一个和该线程绑定的变量的副本。从而隔离了多个线程的数据,每个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步,还提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal。减少了大量的参数传递,使代码简洁,但一个线程会绑定多个自定义的局部变量时,ThreadLocal是抽象在线程上的对象的创建工厂,

    怎么检测一个线程是否拥有锁

    在Thread类中有个方法叫holdsLock()他返回true如果当且仅当当前线程拥有某个具体对象的锁。

    怎么唤醒一个阻塞的线程

    1. 如果线程是因为调用了wait() sleep()或者join()方法而导致的阻塞 ;

      • suspend与resume,不会释放锁。

      • wait与notify 必须配合sychronized使用,因为调用之前必须持有锁,wait会立即释放锁,notity则是同步块执行完才释放锁。

      • await 与singal ,Condition类提供,而Condition对象由new ReenLock.new Condition() 获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法。

      • park 与unpark LockSupport是一个非常方便使用的线程阻塞工具,它可以在任意位置让线程阻塞,和Thread.suspend()相比,他弥补了由于resume()在前发生,导致线程无法继续执行的情况。

        2 如果线程遇到了IO阻塞无能为力,因为Io是操作系统实现的,java代码没有办法接触到操作系统

    java中一个字符占多少字节,int,long double占多少字节

    a 1字节:byte,boolean

    b 2字节 short,char

    c 4字节 int float

    d 8字节 龙 double

    HashCode与equals的相关规定

    1. 如果两个对象相等,则hashcode一定也是相等的。
    2. 两个对象相等,对两个equals方法返回true
    3. 两个对象有相同的hashcode值,他们也不一定是相等的
    4. 综上,equals方法被覆盖过,则hashcode方法也必须背覆盖

​ 5.hashcode的默认行为是对堆上的对象产生独特值,如果没有重写hashcode,则该class的对象无论如何都不会相等(即使这两个对象指向相同的数据)

如果同步块内的线程抛出异常会怎么样

sychroized方法正常返回或者抛异常而终止,jvm会自动释放对象锁

如何检测死锁

1 概念

是指两个或两个以上的进程在执行过程中因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去,此时称系统处于死锁。

四个必要条件

  1. 互斥条件。进程对所分配大盘的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待。直到占有该资源的进程使用完成后释放该资源

  2. 请求和保持条件。进程获得一定的资源后,又对其他资源发出请求但是该资源可能被其他进程占有,此时请求阻塞,但又对自己获得的资源保持不放。

  3. 不可剥夺条件、是指进程获得的资源,在未完成使用之前,不可被剥夺,只能在使用完之后自己释放。

  4. 环路等待条件。是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系。

    原因:因竞争资源发生死锁现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸多资源的竞争而发生死锁现象。

    ​ 进程推进顺序不当引发死锁。

    检查死锁:

    有两个容器,一个用于保存线程正在请求的锁,一个用于保存线程已经持有的锁,每次加锁之前都会做如下检测检测之前正在请求的锁是否已经被其他线程池有,如果有,则把那些线程找出来,遍历第一部中返回的线程,检查自己持有的锁是否正被其中任何一个线程请求,如果第二部返回真,表示出现了死锁。

    死锁解除与防护

    控制不要让四个必要条件成立。

线程池的好处,详解,单例

线程池的好处

池化技术应用:线程池、数据库连接池、http连接池等等。 池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用 率。 线程池提供了一种限制、管理资源的策略。 每个线程池还维护一些基本统 计信息,例如已完成任务的数量。

使用线程池的好处: 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成 的消耗。 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执 行。 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会 消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的 分配,监控和调优。

线程池的并发数可以有效地避免大量的线程池争夺CPU资源而造成的堵塞。

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable>
workQueue,
ThreadFactory
threadFactory,
RejectedExecutionHandler
handler

corePoolSize 线程池中核心线程的数量

maximumPoolSize 线程池中最大线程数量

keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间 超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的 allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的 超时时长 unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等

workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是 尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute 方法提交来的。

threadFactory 为线程池提供创建新线程的功能,这个我们一般使用默 认即可 handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线 程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池 无法处理新线程时,会抛出一个RejectedExecutionException。

这7个参数中,平常最多用到的是corePoolSize、maximumPoolSize、 keepAliveTime、unit、workQueue.在这里我主抽出corePoolSize、 maximumPoolSize和workQueue三个参数进行详解。 maximumPoolSize(最大线程数) = corePoolSize(核心线程数) + noCorePoolSize(非核心线程数); (1)当currentSize=corePoolSize、并且workQueue未满时,添加进 来的任务会被安排到workQueue中等待执行。 (3)当workQueue已满,但是currentSize=corePoolSize、workQueue已满、并且 currentSize>maximumPoolSize时,调用handler默认抛出 RejectExecutionExpection异常。

(1)FixedThreadPool: Fixed中文解释为固定。结合在一起解释固定的线程池,说的更全面 点就是,有固定数量线程的线程池。其 corePoolSize=maximumPoolSize,且keepAliveTime为0,适合线程稳 定的场所。

(2)SingleThreadPool: Single中文解释为单一。结合在一起解释单一的线程池,说的更全面 点就是,有固定数量线程的线程池,且数量为一,从数学的角度来看 SingleThreadPool应该属于FixedThreadPool的子集。其 corePoolSize=maximumPoolSize=1,且keepAliveTime为0,适合线程同 步操作的场所。 (

3)CachedThreadPool: Cached中文解释为储存。结合在一起解释储存的线程池,说的更通 俗易懂,既然要储存,其容量肯定是很大,所以他的corePoolSize=0, maximumPoolSize=Integer.MAX_VALUE(2^32-1一个很大的数字)

4)ScheduledThreadPool: Scheduled中文解释为计划。结合在一起解释计划的线程池,顾名思 义既然涉及到计划,必然会涉及到时间。所以ScheduledThreadPool是一 个具有定时定期执行任务功能的线程池。

线程池大小确定

有一个简单且使用面比较广的公式:

CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线 程数设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了 防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影 响。一旦任务停止,CPU就会出于空闲状态,而这种情况下多出来一 个线程就可以充分利用CPU的空闲时间。

I/O密集型(2N):这种任务应用起来,系统大部分时间用来处理I/O 交互,而线程在处理I/O的是时间段内不会占用CPU来处理,这时就可 以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,可以 配置多一些线程,具体计算方是2N。

RejectedExecutionHandler:饱和策略 当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提 交的任务采用一种特殊的策略来进行处理。这个策略默认配置是 AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

1.AbortPolicy:直接抛出异常

2.CallerRunsPolicy:只用调用所在的线程运行任务

3.DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

4.DiscardPolicy:不处理,丢弃掉。

线程组 线程池(ThreadGroup与ThreadPoolExecutor)

这两个概念: a、线程组就表示一个线程的集合。 b、线程池是为线程的生命周期开销问题和资源不足问题提供解决方案, 主要是用来管理线程

为什么不推荐通过Executors直接创建线程池

缓存队列 LinkedBlockingQueue 没有设置固定容量 大小

8.1.1Executors.newFixedThreadPool() 创建固定大小的线程池 ThreadPoolExecutor 部分参数:

corePoolSize :线程池中核心线程数的最大值。此处为 nThreads个。

 public static ExecutorService newFixedThreadPool(int nThreads) { 
 return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); } 

maximumPoolSize :线程池中能拥有最多线程数 。此处为 nThreads 个。 LinkedBlockingQueue 用于缓存任务的阻塞队列 。 此处没有设置容量 大小,默认是 Integer.MAX_VALUE,可以认为是无界的。

问题分析: 从源码中可以看出, 虽然表面上 newFixedThreadPool() 中定义了 核心 线程数 和 最大线程数 都是固定 nThreads 个,但是当 线程数量超过 nThreads 时,多余的线程会保存到 中,而 LinkedBlockingQueue 没是无界的,导致其无限增大,最终内存撑爆

Executors.newSingleThreadExecutor() 创建单个线程池 ,线程池中只有一个线程。

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L,
TimeUnit.MILLISECONDS,
new
LinkedBlockingQueue<Runnable>()));
}

创建单个线程池 ,线程池中只有一个线程。 优点: 创建一个单线程的线程池,保证线程的顺序执行 ; 缺点: 与 newFixedThreadPool() 相同。 总结: newFixedThreadPool()、newSingleThreadExecutor() 底层代码 中 LinkedBlockingQueue 没有设置容量大小,默认是 Integer.MAX_VALUE, 可以认为是无界的。线程池中 多余的线程会被缓 存到 LinkedBlockingQueue中,最终内存撑爆

最大线程数量是 Integer.MAX_VALUE 8.2.1Executors.newCachedThreadPool() 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new
SynchronousQueue<Runnable>());
}

ThreadPoolExecutor 部分参数: corePoolSize :线程池中核心线程数的最大值。此处为 0 个。 maximumPoolSize :线程池中能拥有最多线程数 。此处为 Integer.MAX_VALUE 。可以认为是无限大 。 优点: 很灵活,弹性的线程池线程管理,用多少线程给多大的线程池,不 用后及时回收,用则新建 ; 缺点: 从源码中可以看出,SynchronousQueue() 只能存一个队列,可 以认为所有 放到 newCachedThreadPool() 中的线程,不会缓存到队列 中,而是直接运行的, 由于最大线程数是 Integer.MAX_VALUE ,这个数 量级可以认为是无限大了, 随着执行线程数量的增多 和 线程没有及时结 束,最终会将内存撑爆。

Executors.newScheduledThreadPool() 创建固定大小的线程,可以延迟或定时的执行任务

 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

优点: 创建一个固定大小线程池,可以定时或周期性的执行任务 ; 缺点: 与 newCachedThreadPool() 相同。 总结: newCachedThreadPool()、newScheduledThreadPool() 的 底层代码 中 的 最大线程数(maximumPoolSize) 是 Integer.MAX_VALUE,可以认 为是无限大,如果线程池中,执行中的线程没有及时结束,并且不断地有 线程加入并执行,最终会将内存撑爆

拒绝策略不能自定义

、它们统一缺点:不支持自定义拒绝策略。 Executors 底层其实是使用的 ThreadPoolExecutor 的方式 创建的,但 是使用的是 ThreadPoolExecutor 的默认策略,即 AbortPolicy。

private static final RejectedExecutionHandler
defaultHandler =
new AbortPolicy();
8.4创建线程或线程池时请指定有意义的线程名称,方便出
错时回溯(这个不是重点)
第九节 不怕难之BlockingQueue及其实现
9.1 前言
BlockingQueue即阻塞队列,它是基于ReentrantLock,依据它的基本原
理,我们可以实现Web中的长连接聊天功能,当然其最常用的还是用于实
现生产者与消费者模式,大致如下图所示:
//构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable>
workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue,
Executors.defaultThreadFactory(),
defaultHandler);
}

LinkedBlockingQueue

LinkedBlockingQueue中维持两把锁,一把锁用于入队,一把锁用于出 队,这也就意味着,同一时刻,只能有一个线程执行入队,其余执行入队 的线程将会被阻塞;同时,可以有另一个线程执行出队,其余执行出队的 线程将会被阻塞。换句话说,虽然入队和出队两个操作同时均只能有一个 线程操作,但是可以一个入队线程和一个出队线程共同执行,也就意味着 可能同时有两个线程在操作队列,那么为了维持线程安全, LinkedBlockingQueue使用一个AtomicInterger类型的变量表示当前队列 中含有的元素个数,所以可以确保两个线程之间操作底层队列是线程安全 的

ArrayBlockingQueue

源码分析 ArrayBlockingQueue底层是使用一个数组实现队列的,并且在构造 ArrayBlockingQueue时需要指定容量,也就意味着底层数组一旦创建 了,容量就不能改变了,因此ArrayBlockingQueue是一个容量限制的阻 塞队列。因此,在队列全满时执行入队将会阻塞,在队列为空时出队同样 将会阻塞

深入理解ReentrantLock与Condition

锁是什么

并发编程的时候,比如说有一个业务是读写操作,那多个线程执行这个业 务就会造成已经写入的数据又写一遍,就会造成数据错乱。 所以需要引入锁,进行数据同步,强制使得该业务执行的时候只有一个线 程在执行,从而保证不会插入多条重复数据。 一些共享资源也是需要加锁,从而保证数据的一致性

锁的相关

线程安全

一段代码所在的进程中有多个线程在同一时间段内运行,而这些线程可能会同时运行这段代码。每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说,一个类或者程序所提供的接口,对于线程来说是不可拆分的操作; 或者多个线程之间的切换不会导致该接口的执行结果存在差异, 这也是线程安全的。
其实线程安全不是一个“非黑即白”单项选择题。按照“线程安全”的安全程度由强到弱来排序可分为5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立.

  • 不可变
    java中不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施.如final关键字修饰的数据不可修改,可靠性最高。String 类是不可变的,故是线程安全的。

  • 绝对线程安全
    绝对的线程安全,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施,通常需要付出很大的代价。

  • 相对线程安全(这是我们要达到的目的, 相对安全即可)
    相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。
    是指线程单独运行是安全的, 但是涉及多个线程交互就不安全了.
    Java中大部分的线程安全类都属于相对线程安全的,如Vector、HashTable、CouncurrentHashMap, Collections的synchronizedCollection()方法保证的集合。

  • 线程兼容(也常见这种情况)
    线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
    线程兼容是指线程单独运行不安全, 涉及线程交互也不安全。
    Java中大部分的类都是属于线程兼容的, 如ArrayList和HashMap等。

  • 线程对立
    线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。

    线程安全问题产生的原因

    究其本质, 是因为多条线程操作同一数据的过程中,破坏了数据的原子性.

    所谓原子性,就是不可再分性. 换句话说就是当前线程在操作这一数据的过程中, 被其他线程打断或者抢占了数据, 这就是破坏了数据的原子性.

    • 线程安全问题都是由全局变量及静态变量引起的, 也就是所谓的公共变量.

    • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的.

    • 若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全.

      如何保证线程安全

      • 线程的安全性问题体现
      1. 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性, 线程切换会破坏原子性.

      2. 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到.

      3. 顺序性 :禁止指令重排?

        解决办法

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9dH3zrG-1647698670680)(E:\blog\source\imges\20210216112023540.png)]

Synchronized

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized 属于 重量级锁,效率低下.

在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了. JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
可以修饰普通方法(锁是实例对象)静态方法(类对象)代码块(类对象)

Lock

在java.util.concurrent.locks 包下, 提供了Lock和ReadWritelLock两个接口
Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0x2RQcLT-1647698670681)(E:\blog\source\imges\20210114174820858.png)]

  • Lock.RenntrantLock
    其中 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的 。

  • ReadWriteLock.ReentrantReadWriteLock
    ReentrantLock是一种排他锁,同一时刻只允许一个线程访问,ReadWriteLock 接口的实现类 ReentrantReadWriteLock 读写锁提供了两个方法:readLock()和writeLock()用来获取读锁和写锁,也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

    synchronized关键字和Lock锁区别

    • 首先synchronized是java内置关键字,在jvm层面Lock是个JUC(java.util.concurrent)的java类, ReentrantLock继承了Lock锁.

    • Lock锁会让JVM花较少的时间调度线程,性能更好,子类多扩展性好.
      优先顺序 Lock锁 > synchronized (obj) {…} > synchronized func()

    • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

    • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁). Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

    • synchronized的锁是可重入锁、不可中断, 非公平锁. 而Lock锁可重入、可中断(两者皆可), 公平锁, 排他锁.

    • Lock只有代码块锁,而 synchronized 有代码块锁和方法锁.
      Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

      CAS算法

      CAS全称为Compare And Swap即比较并交换,其算法公式如下:CAS(V,E,N)
      CAS指令指令需要有3个操作数.

      内存地址(在java中理解为变量的内存地址,用V表示)

      旧的预期值(用E表示)
      新值(用N表示).
      CAS指令执行时,当且仅当V处的值符合旧预期值E时, 处理器用N更新V处的值, 否则它就不执行更新, 但是无论是否更新了V处的值, 都会返回V的旧值, 上述的处理过程是一个原子操作
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZI8pvtGx-1647698670682)(E:\blog\source\imges\20210103172011999.png)]

JDK里面提供了很多Atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。它们是通过CAS完成原子性

  • 我们来看下AtomicInteger的incrementAndGet()方法
    在这里插入图片描述

  • 再看getAndAddInt()方法:
    在这里插入图片描述

  • 这里调用了compareAndSwapInt()方法, 所以这些Atomic类是线程安全的

    volatile关键字

    在多个线程读取共享变量时, 能保证每个线程读取到共享变量的最新值.
    代码在执行的过程中的先后顺序, Java 在编译器以及运行期间的优化, 代码的执行顺序未必就是编写代码时候的顺序. volatile 关键字可以禁止指令进行重排序优化, 保证代码按着编写的顺序执行. 最典型的应有就是双重校验锁实现对象单例(线程安全)

    public class Singleton {
        private volatile static Singleton uniqueInstance;
        private Singleton() {
        }
        public static Singleton getUniqueInstance() {
           //先判断对象是否已经实例过,没有实例化过才进入加锁代码
            if (uniqueInstance == null) {
                //类对象加锁
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    
    

    需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
    uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
    为 uniqueInstance 分配内存空间
    初始化 uniqueInstance
    将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
    使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

    synchronized和volatile的区别

    synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

    • volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比synchronized关键字要好。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码

    • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

    • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

      ThreadLocal

      ThreadLocal的理解

      • ThreadLocal是每个线程内部的一个局部变量, 能够保证在读取公共变量时, 每个线程互不干扰, 达到了线程隔离的作用.

      • 每个Thread内部有一个ThreadLocalMap类型的成员变量threadLocals, ThreadLocalMap内部有个Entry[]数组,用于存放绑定的值。

      • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap中获取value。我们可以简单的这样理解,ThreadLocal对象就好比是一个用于给Thread对象绑定值的工具类,当调用ThreadLocal对象的set(v)方法时,它会先获取当前线程对象thread,然后找到thread对象自己内部的ThreadLocalMap对象 map,然后把ThreadLocal对象作为key,v作为值存进map中,map中是用Entry。

      • ThreadLocal的内存泄漏问题避免
        从源码中我们可以看到,ThreadLocal对象是被Thread#ThreadLocalMap#Entry对象以弱引用的方式引用的,也就是entry对象是个弱引用.
        那么当把ThreadLocal设置为null时,ThreadLocal对象的可见行就变成了弱可见,弱可见对象的特点是当GC发现时entry对象就会被回收.
        但是ThreadLocalMap生命周期和Thread是一样的,只要Thread还运行就不会被GC回收,这时候就会导致Thread#ThreadLocalMap#Entry这个对象继续存在, 但因为ThreadLocal对象为null了,也就是entry对象中的key为null, 也就是entry对象无法被使用了,这就是所谓的内存泄漏。
        简单说就是,如果把ThreadLocal对象设置为null了,但是通过其绑定的value还依然绑定在Thread中,但是却无法使用这个值,这就是所谓的内存泄漏。
        解决办法:在设置ThreadLocal为null前,先执行ThreadLocal的remove操作,把值销毁,避免出现内存溢出情况。

        ThreadLocal和volatile的区别

        ThreadLocal是线程的局部变量, 而volatile是关键字
        ThreadLocal能保证线程的可见性, 是保证多个线程能读取共享变量的初始值, 而且能保证线程隔离, 各个线程之间操作此共享变量互不影响.
        volatile也能保证线程的可见性, 但是它保证的是多个线程读取到共享变量的最新值.可以理解为和ThreadLocal是相反的.

        总结

        综上所述,

        synchronized/Lock锁/CAS算法能保证线程的原子性, 可见性, 顺序性.大量同步代码使用Lock锁, 少量同步代码使用synchronized关键字.
        ThreadLocal和volatile能够保证线程的可见性, volatile还能保证线程的顺序性.
        其实JUC(java.util.concurrent)java并发包下提供了挺多保证线程安全的工具, Lock锁就是JUC并发包下的接口, 还有Atomic原子类, volatile关键字, ConcurrentHashMap等.

        在这里插入图片描述

乐观锁和悲观锁

悲观锁:

总是假设最坏的情况,每次去读数据的时候都认为别人会修改,所以 每次在读数据的时候都会上锁
这样别人想读取数据就会阻塞直到它获取锁 (共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
传统的关系型数据库里边就用到了很多悲观锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

Java中 synchronized 和 ReentrantLock(可重入锁) 就是悲观锁思想的实现

乐观锁

  • 乐观锁认为自己在读数据时不会有别的线程修改数据, 所以不会添加锁,.
  • 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据.
  • 如果这个数据没有被更新, 当前线程将自己修改的数据成功写入. 如果数据已经被其他线程更新, 则根据不同的实现方式执行不同的操作(例如报错或自动重试)

实现方式

cas算法

在Java中 java.util.concurrent.atomic包下面的原子变量类 就是基于CAS实现的乐观锁.
CAS并不是一种实际的锁, 它仅仅是实现乐观锁的一种思想, java中的乐观锁(如自旋锁)基本都是通过CAS操作实现的. CAS是一种更新的原子操作, 比较当前值跟传入值是否一样, 一样则更新, 否则失败.
CAS全称为Compare And Swap即比较并交换,其算法公式如下:
函数公式:CAS(V,E,N) V:表示要更新的变量, E:表示预期值, N:表示新值
在这里插入图片描述

数据库的版本控制(javaee)

应用场景
  • 悲观锁适合写操作多的场景, 先加锁可以保证写操作时数据正确.
  • 乐观锁适合读操作多的场景, 不加锁的特点能够是其读操作时性能大幅度提升.

死锁和活锁

死锁:当多个线程循环等待彼此占有的资源释放, 而无限期的僵持等待下去的局面.

死锁产生的原因

(1)竞争资源, 系统中的资源可以分为两类:
可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和内存均属于可剥夺性资源;
另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁.
(2)死锁产生的必要条件, 也就是需要同时具备以下4个条件, 才会有死锁现象, 如果有一项不满足也是不会出现死锁的.
互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
循环等待:发生死锁时,线程进入死循环,永久阻塞。

解决死锁

破坏互斥条件 : 这个条件我们没有办法破坏, 因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件

活锁

活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功

自旋锁 和适应性自旋锁

自旋锁

线程在获取资源的时候, 资源被占用, 为了让当前线程, “稍微等一下”, 我们需让当前线程进行自旋, 也就是等待着

如果在自旋完成后, 前面锁定同步资源的线程已经释放了锁, 那么当前线程就可以不必阻塞而是直接获取同步资源, 从而避免切换线程的开销, 这就是自旋锁

img

自旋锁不能替代阻塞, 自旋等待虽然避免了线程切换的开销,但它要占用处理器时间.
如果锁被占用的时间很短, 自旋等待的效果就会非常好; 反之, 如果锁被占用的时间很长, 那么自旋的线程只会白浪费处理器资源.
所以自旋等待的时间必须要有一定的限度, 如果自旋超过了限定次数(默认10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁, 就应当挂起线程

适应性自旋锁

那自旋次数还要手动更改,效率不高,所以JDK1.6以后就引入了自适应的自旋锁(适应性自旋锁).

自适应性意味着自旋的时间(次数)不再固定, 而是前一次在同一个锁上的自旋时间及锁的拥有者的转态来决定.
如果在同一个锁对象上, 自旋等待刚刚成功获得过锁, 并且持有锁的线程正在运行中, 那么虚拟机就会认为下次自旋也是很有可能再次成功, 而且允许它有次数较多的自旋次数.
如果对于某个锁, 自旋很少成功获得过, 那么下次自旋则认为它失败几率大, 直接阻塞线程, 避免浪费处理器资源

无锁 偏向锁 轻量级锁 重量级锁

jDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
在这里插入图片描述

可重入锁VS非可重入锁

一个线程中有多个子流程, 而资源只有一个, 那么这些子流程如何和资源锁定呢? 所以这时候又引入了可重入锁和非可重入锁.

  • 可重入锁又名递归锁, 是指在同一个线程在外层方法获取锁的时候, 再进入线程的内层方法会自动获取锁(不过前提是锁对象也就是资源是一个对象或class). 不会因为之前获取过还没释放而阻塞. 可重入锁的优点就是: 可一定程度避免死锁
  • 非可重入锁, 就是指在同一个线程在外层方法获取锁的时候, 再进入线程的内层方法必须等外层方法释放锁才能获取, 如果外层方法没有释放锁, 就会出现死锁现象.

可中断锁

可中断锁:顾名思义, 就是可以相应中断的锁.在Java中, synchronized就不是可中断锁, 而Lock是可中断锁, Lock.lockInterruptibly()就可以中断锁.
如果某一线程A正在执行锁中的代码, 另一线程B正在等待获取该锁, 可能由于等待时间过长, 线程B不想等待了想先处理其他事情, 我们可以让B中断自己或者在别的线程中中断它,这种就是可中断锁。
共享锁(读锁)VS独享锁(排他锁或写锁)
多个线程能不能共享一把锁呢?如果能就是共享锁, 如果不能就是独享锁.

共享锁 独享锁(排他锁或写锁)

共享锁顾名思义就是某一资源可以被多个线程公用, 共享锁又名写锁, ReadWriteLock中的readLock()方法获取读锁.
独享锁也叫排他锁或者写锁, 是指该锁一次只能被一个线程所持有. 如果线程A对数据T加上排他锁后, 则其他线程不能再对T加任何类型的锁, 获得排他锁的线程即能读数据又能修改数据, synchronized就是排他锁, 还有ReadWriteLock的writeLock()能获取写锁.
不过ReadWriteLock读写锁使用时是一个整体, ReadWriteLock其实是互斥锁, 它内部有readLock()和writeLock()两个方法分别获取读锁和写锁, 实现读读数据共享, 读写, 写读,写写过程资源不共享保证线程安全.

公平锁和非公平锁

如果一个线程组里, 能保证每个线程都能拿到锁, 那么这个锁就是公平锁. 相反, 如果保证不了每个线程都能拿到锁, 也就是存在有线程饿死, 那么这个锁就是非公平锁.
synchronized就是非公平锁, Lock.ReetrantLock可以有公平锁和非公平锁.
ReentrantLock虽然有公平锁和非公平锁两种, 但是它们添加的都是独享锁.
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间.
使用场景的话呢,其实还是和他们的属性一一相关,举个栗子:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁会给业务增强很多的可控制性。

相关问题(待更新)

1 请描述sychronized和reentrabtlock的底层实现及重入的底层原理 请描述下sychronized和 reentrabtlock的异同

2 请描述锁的四种状态和升级过程

3 cas的aba问题如何解决

4 AQS的底层是cas+volatile

5 请谈一下对volatile的理解

6 cas是什么

7 请描述下对像的创建过程

8 对象在内存中的内存布局

9 DCL单例为什么要交volatile

10 object o = new Object() 在内存中占了多少字节

11 ThreadLocal 的了解 threadlocal如何解决内存泄露问题

12 描述下锁的分类以及jdk中的应用

13 自旋锁一定比重量级锁效率高吗

14 打开偏向锁是否效率一定会提升?为什么
Java的基本数据类型

字符串数字转换为数字

字符串转为数字原理

-12.5四舍五入的结果

非静态内部类创建静态实例会不会导致内存泄漏

JAVA的多态

常见的Exception

多态的作用

单例能否继承(对象初始化过程)

静态变量什么情况下会放在内存里

有一个Long类型的静态变量,创建静态方法循环对静态变量进行++一百次,用一百个子线程调用这个方法,执行结果Long能不能确定,为什么这个为什么线程不安全如果方法里面i=i+1 会有什么不一样 这个Long的范围是多少 对这个静态变量加volitile关键字以后值能不能确定,把方法改成i=i+1 值会不会有问题,这个Long的值范围是多少

静态变量什么情况下会回收

==和equals

String a = “abc”;String b = “abc”, a==b ?

String c = new String(“abc”), a==c ?

重写equals的注意事项

hashcode的原理

hashcode注意事项

两个相等的对象hashcode值一样吗

抽象类和接口的区别

interface的关键字default的了解

JDK为什么提供interface的defalut

面向对象的特征

JAVA四种引用的区别

非静态内部类Handler持有外部类引用的解决方法

静态内部类以弱引用方式持有外部引用解决交互和内存泄漏

成员变量 、局部变量区别(局部变量需要默认值)这个问题答反了

String的最大长度

==和equals

Java的引用类型

java string Stringbuffer StringBuiffer 区别

弱引用使用场景 弱引用

JAVA的四种引用

枚举和常量的性能区别

静态内部类和普通内部类的区别

不使用反射的动态代理模式

JVM内存模型内存回收

Class字节码的理解

反射获取方法并调用

JAVA的类加载机制

JAVA的GC内存模型

为什么JDK1.8使用新生代老生代内存模型

为什么新生代回收使用复制算法老年代不使用

JAVA虚拟机内存分布

JAVA虚拟机内存中哪些是线程共享哪些不共享

try catch 捕获 Error

try catch跨线程捕获异常

StringBuffer和StringBuilder的区别

StringBuffer线程安全原理

String线程安全

String可修改

静态变量,静态代码块,普通变量,普通代码块,执行顺序

静态变量,静态代码块,普通变量,普通代码块,构造函数执行顺序

equals和==的区别

基本数据类型equals和==的区别

内存泄露

内存泄漏

native内存泄漏方案

常见的内存泄漏场景

JVM内存分区

自定义注解

JVM垃圾回收算法

JAVA中GC如何判断对象被回收

新生代老年代算法

启动优化的数据分析

集合原理

hashmap

HashMap实现原理

创建HashMap要放入1000个不同hashCode的键值对,初始最大值多少

为什么会有hash冲突

为什么HashMap使用红黑树不用二叉树

不考虑碰撞的HashMap时间复杂度

HashMap为什么使用红黑树 对红黑树的理解 HashMap的时间复杂度

HashCode的计算方法

HashMap如何使用HashCode计算下标

HashMap插入元素的原理

HashMap中数据结构使用红黑树的原理

arrayList

ArrayList底层数据结构

ArrayList的初始容量

ArrayList的remove方法原理

ArrayMap

ArrayMap和HashTable

ArrayList的增删改查时间复杂度

ConcurrentHashMap原理
SparseArray的理解

手写JAVA单例模式

锁的升级过程

sychronized升级过程

synchronized保证线程执行顺序

synchronized关键字及升级过程 synchronized使用及锁对象与竞争

synchronized修饰方法和代码块的区别

synchronized静态方法和普通方法的区别

synchronized使用及区别

synchronized可用范围

volitile原理

volitile内存屏障

volitile如何保证可见性

volitile如何保证有序性

volitile的理解 volitile特点 DCL不加volitile可能会有什么问题

DCL单例中volitile的作用

DCL单例中两次判空的原因

设计一个volitile使用场景

volitile在底层实现

CPU如何保证缓存一致性

一个CPU的缓存发生了改变,如何通知其他CPU进行改变

保证线程顺序的方案

对Lock接口的了解

假定一个可能用到指令重排序的场景

在B线程中中断有死循环的A线程

Object的wait和notify方法

Object的notifyAll和notify方法的区别

CAS作用

乐观锁

同步方法继承

Unsafe的了解

线程安全的集合

死锁的原理

JVM

JVM内存模型

两个对象相互引用是否会标记为垃圾

哪些对象作为GCRoot

JVM内存模型分为哪几块

JVM虚拟机和Dalvik虚拟机的区别

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小阳世界2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值