java面试

Java 基础

Java 面向对象有哪些特征

封装、继承、多态、(抽象)

什么是浅拷贝什么是深拷贝

11
浅拷贝指对内存地址的拷贝,如=
深拷贝指对对象属性进行拷贝,如 beanUtil

hashcode、==和 equals 的区别

hashcode 求值的散列表值,相同值的哈希值一定相等,不同值的哈希值可能相等
==是比较内存地址
equals 是比较对象,默认不重写的情况下,和==无区别,重写 equals 一般是通过==比较对象里的值是否相等,且重写也要重写 hashcode,否则在将对象存入 hashmap 这样的表时,hashcode 会取内存地址来求哈希值,进而导致不同对象可能存同一 hash 索引位置

hashcode 是求键的位置之和,求哈希码,==是比值,equals 是比对象

equals 相等/不等,hashcode 和==一定相等/不等
==相等/不等,hashcode 和 equals 不一定相等/不等
hashcode 相等/不等,equals 和==不一定相等/不等

java 代理模式有哪些

  • 静态代理:直接在类里写函数
  • 动态代理:基础接口,实现接口的函数
  • CGLIB 动态代理:导入 cglib 依赖,创建代理类和工厂类,通过反射来代理

jdk 动态代理和 CGlib 动态代理的取别

  • 只有有接口的类才能使用 jdk 动态代理,它会使用代理类来操作
  • CGLIB 是没有接口的类会使用这种方法代理,spring 会将类作为父类来生成对于的子类,由子类重写父类方法从而增强补充父类方法.但是如果类被 final 修饰则无法使用 CGLIB 动态代理
  • 理论上,CGLIB 生成代理类慢,调用快,jdk 动态代理生成快,调用慢

异常处理有那种方式

  • throw:抛出异常,catch(Exception e){throw new Exception("XXX");}
  • throws:声明异常,public int xx(int x, int y) throws xxException {…};
  • try,cath,finally:捕获异常:try..cath..finally

重写和重载的区别

  • 重载:同一类中,相同的方法不同的参数
  • 重写,子类修改父类的方法

String、StringBuffter、StringBuilder 的区别和使用场景

  • String 是一个字符串类,其创建的值被放入字符串池,是不可变的,对象的值改变是因为改变了指针
  • StringBuffter 和 StringBuilder 其创建的值都是可变的,其原理是创建长度固定的数组,向数组里插入字符,数组满了再创建更大的数组,再插入
  • StringBuffter 使用了 synchronized 保证线程安全,但效率低

一个类如何避免被继承

使用 final 修饰的类不能被继承

一个类的构造方法被 private 修饰,如何获取这个类的对象

将这个类用 static 修饰

通过反射暴力破解

讲讲你对反射的理解/为什么使用反射

反射是获取类对象的一种方法,这种方法提出了一种叫反射机制的概念,其符合开闭原则,即不改变源代码的情况下对原有功能进行拓展,可以通过反射机制而不改变源码来读取操作任意类.

反射通常使用在框架的使用上,在 bean 初始化时,其通过反射获取 bean 对象并存储在集合中.在 RPC 里,用反射调用远程传递过来的类对象并进行操作

讲讲你对 static 的理解

static 可以用来修饰类、函数或变量,让其具有类基本的属性,在类的生命周期中,如果一个变量被 static 修饰,那么其会在初始化阶段被赋予默认值.且如果一个变量是类变量,那么该类下的成员函数和成员变量都可共享使用该类变量

自定义异常如何使用

继承 RuntimeException,使用 throw new 自定义异常();抛出即可

RuntimeException 和 Exception 的区别

RuntimeException 是 Exception 的子类,代表运行时出现的异常

Exception 的子类包括:

  • RuntimeException:运行时的异常
  • IOException:读写时的异常

布尔类型可以和整数类型比较吗

使用==比较不可以,只有 c++可以
使用 equals 比较指针的话,可以

调用完的方法如何被回收

方法存放在栈区,调用完成后出栈回收

父类有构造函数,子类如何继承

若父类构造函数无参,子类直接继承即可,默认会添加上 super();
若父类构造函数有参,子类继承时需要加入 super(参数);

讲讲你对泛型的理解/为什么要使用泛型

由于集合存储时不知道对象的类型,会统一以 Object 类型存储,这会导致一些类型安全问题.因此,jdk1.5 后引用了一种泛型的设计,可以指定集合的存储类型,但是其只在编译时起作用,运行时没有泛型的概念.通过指定对象类型来避免类型安全和强制转换的问题.

泛型一个重要的知识点是类型擦除和泛型擦除,当一个具有泛型的集合赋值给一个无泛型的集合,其泛型和内部存储的对象类型会向上转换,如 List1=List2,那么 List1 存储的对象都是 Object,原来的泛型和类型都被擦除了

泛型可以通过使用<? super T><存储的未知类型必须是 T 的父类>指定上限或<? extend T>(存储的未知类型必须是 T 的子类)指定下限

JDK1.8 有什么新特性

  • 新增了 Lambda
  • 为了兼容 Lambda,接口里面可以写方法,写的是默认方法
  • 新增了函数式接口
  • stream 接口
  • 多重注解
  • jvm 回收算法

抽象类和接口类有什么异同

同:

  • 都不能实例化
  • 可以作为引用类型
  • 基础抽象或接口的话,其方法必须全部实现,否则还需要继续抽象

接口:

  • 不能定义构造方法
  • 成员变量类型可以是 private、public、protected
  • 一个类可以有多个接口
  • 可以有静态方法

抽象:

  • 可以定义构造方法
  • 成员变量类型只能是 public
  • 一个类只能由一个抽象
  • 不能有静态方法

java 中有哪些新建对象的方法

  • new
  • 反射 newinstance
  • 序列化
  • Object.clone
  • 静态方法

讲讲包装类的装箱拆箱

  • 装箱:将基本类型变为对象,jdk1.5 之前是包装类(值),jdk1.5 后新增方法包装类.ValueOf(值)
  • 拆箱:将对象变为基本类型,使用包装类.XXValue()
  • 自动装箱:jdk1.5 后,可以直接将基本类型赋值给包装类,内部会通过调用 ValueOf(值)来实现,如果这个值在-128~127 之间,还会将其缓存,下一次创建不会新建新的对象,而是调用缓存的对象
  • 自动拆箱:当基本类型和包装类进行比较时,包装类会进行拆箱

集合

你认识的集合有哪些

java 中大多数集合都是由 Colletions 和 Map 这个接口衍生而来的,而 Colletion 由衍生了 Queue、List 和 Set,因此主要为

  • Queue:队列,现先进先出
  • List:有序可重复列表
  • Set:无序不可重复列表
  • Map:具有映射关系的列表

常用的有 ArrayQueue、ArrayList、LinkeList、HashSet、TreeSet、HashMap、LinkedHashMap、TreeMap

特殊的有:LinkedBlockingQueue、ArrayBlockingQueue、HashTable、ConcurrentHashMap、ThreadHashMap

有序的就一定有Array,数组的有Linke,无序的一定有Tree和Hash

Java 中的队列有哪些,一般用在哪里

  • LinkList
  • ArrayBlokingQueue:有界队列,有大小限制
  • LinkBlokingQueue:无界队列,无大小限制

一般用在线程池

ArrayList 查的快,存的慢,原理是数组
LinkList 存的快,查的慢,原理是链表

高并发的集合有哪些问题

高并发集合会出现线程安全问题,一般采用锁或者线程安全的集合

最初版本的 jdk 线程安全集合:Vector、Hashtable

常用的非线程安全集合:ArrayList,HashMap
若想要将 ArrayList,HashMap 改为线程安全,可使用 Collections.synchronized(list)

部分集合在内部实现了各种锁来实现线程安全,如:
当前常用的线程安全集合:java.util.concurrent、CopyOnwriteArrayList、ConcurrentHashMap
而线程池常用的安全线程队列集合是:LinkBlockingQueue 和 ArrayBolckingQueue

讲讲 HashMap 原理

jdk1.7 以前 HashMap 由哈希表(数组)+链表
jdk1.7 以后由哈希表(数组)+链表/红黑树

其原理是通过键.hashcode()获取其哈希值,通过取模或高平低位算法获取对应下标存在数组里,默认数组长度是 8,不够会自动扩容,也可以自定义默认大小,将需要存的对象键值以链表形式存储,若链表长度超过 8,会将链表装为红黑树

对于防止 hash 冲突,官方使用的是高平低位算法,即生成的哈希码是 int32 位,其中前 16 高位和后 16 低位进行异或,这样可以增加 hash 值的范围,从而避免 hash 冲突

hashmap 的自动扩容根据其负载因子决定,其负载因子为 0.75,即若数组存了 3/4 的数据时就进行自动扩容 2 倍,扩容机制和 arraylist 类似,也是新建数组,然后复制数据.其复制数据前会重新计算每个链表或数的节点的 hash 值(这也是为什么节点会存 key,是因为方便重写计算 hash 值),然后 jdk1,7 以前其扩容复制使用的头插法将数据插入,但是头插法在多线程环境下可能会导致首尾相连,导致死循环,所以 jdk8 以后使用的是尾插法,一般情况下不建议触发 hashmap 的自动扩容机制,建议通过 new hashmap(int n)构造函数直接指定数组大小,默认数组大小是 8

红黑树转换阈值是 8,其实 6 开始,红黑树的速度就已经快于链表,但为了防止频繁转换红黑树,所以设置为 8

hashmap 为什么不用平衡树,用了红黑树

讲讲 Arraylist、TreeMap、HashSet、LinkedHashMap 的原理

Arraylist 其底层是一个数组,相比普通数组其封装了一个自动扩容的机制,即当插入新数据时,会先判断数组是否满了,若满了则新建一个比原来大 1.5 倍的数组,把原数据复制进来,再把新数据插入

TreeMap 其底层是红黑树,每个节点存对于的键值对,当插入新数据的时候会与键进行比较,比键大的放左边,比键小的放右边,键必须重写 comparator 比较方法

HashSet 其继承 HashMap,使用 HashMap 的哈希表(数组)来存值

LinkedHashMap 其在 HashMap 基础上进行了修改,将链表改为双向链表,且每个数组索引存的链表都是连接起来的,保证了数据的有序性,可从链表直接找完所有数据,不需通过索引找

HashMap 和 Hahtable/CurrentHashMap 区别

  • HashMap 是不安全的,Hashtable 是安全的
  • HashMap 键值允许 null,当键为 null 则把数据存在数组下标 0,不为 null 则取 key.hashCode()它的哈希值,而 HashTable/CurrentHashMap 直接使用 key.hashCode(),没有判断是否为 null,所以 HashTable/CurrentHashMap 键使用 null 会报空指针异常
  • Hashtable/CurrentHashMap 之所以不允许 null 为键,是因为在多线程中,无法判断键为 null 是没有这个键值对,还是有键值对只是值为 0.HashMap 中允许键为 null 是因为它不用考虑多线程环境,没有锁,可以直接使用 contains(key)来判断属于哪种情况,但加锁后的多线程不能使用 contains(key)

讲讲 ConcurrentHashMap7 的原理

concurrentHashmap7 是将 hashtable 切分成了多个小分段,这些分段用 segment 数组来存储,不同的是 hashtable 用的是 synchnized 锁,concurrenthashmap7 用的是 reentranlock 锁

segment 数组不能扩容,初始化后就固定大小,但是每个 hashmap 片段都可以扩容,扩容机制和 hashmap 相同,唯一有区别的是当扩容的时候,会尝试获取 2 次 hashmap 大小,两次不同,则加锁后再获取一次

concurrenthashmap 在使用 get 的时候没有用上锁,只有在 Put 的时候才会加锁,避免多线程访问相同的 segment 索引,其获取索引的方式也和 hashmap 一样,sement 根据哈希值取模,hashmap 再根据哈希值取模.

讲讲 ConcurrentHashMap8 的原理

concurrenthashmap8 和 concurrenthhashmap7 完全不同,它没有使用 sgement 分段锁,没有使用 sgement 不代表完全删除了它,为了避免 jdk7 升级到 jdk8 发现没有 sgement 对象而报错,所以保留了 sgement,但是没有用上.concurrenthashmap8 在获取数据时给具体链表或红黑树加锁,不同 hashtable 的是 hashtable 是给整个 hashtable 加锁,这只是给哈希表存储的数据加锁,其他功能

讲讲 ConcurrentHashMap 如何做缓存

Current 的键存缓存的 id,值使软引用对象,软引用对象存需要缓存的数据和自身的 key,并使用引用队列存软引用对象

当需缓存一个数据的时候,先判断引用队列是否可弹出软引用对象,可弹出则说明该软引用对象内部存的对象被回收,则根据软引用对象的 key 删除 Map 对应的键值

讲讲 ThreadLocalMap 的原理

ThreadLocalMap 通常是结合 ThreadLocal 使用的,每个线程的哈希值为键,值为线程希望存储的数据.当要通过 set 存储数据时,会获取线程对象,再获取线程对象的哈希值,根据哈希值将值存储进去

若遇到哈希冲突问题,ThreadLocalMap 使用开放空间法,通过增加哈希值走下一个索引,判断下一个索引是否被占用,无占用就存储,get 的时候也是,判断存储的键和线程的键是否一致,不一致走下一个索引

当 set 结束后会对集合进行扫描,若存储长度大于负载因子 2/3 时,则扩容 2 倍,这和 HashMap 类似

hashmap 如何避免内存泄漏

hashmap 存的对象一定要重写其 equals 和 hashcode 方法,这样不同对象但值相同覆盖存储而不是重复存储

高级一点可以与面试官说,既然要考虑 hashmap 的内存泄漏问题,那么 hashmap 在这个程序中就由担当缓存的功能,可以讲上文的 hashmap 做缓存

IO

什么是 IO,NIO 又是什么,AIO 又是什么

IO 是一种数据流,可以用来处理文件、网络,在 java 中,提供字符流和字节流来进行文件的操作,提供 Soket 方式来操作网络流

但是 IO 在处理一个数据流的时候,线程是被占用的,不能去操作另一个新来的数据流,因此 IO 也就 BIO,是一种堵塞 IO,而 NIO 是一种非堵塞 IO,通过使用 Channel、Selector、Buffter 来搭建多路复用机制,去实现非堵塞的 IO.因此 NIO 常用于网络请求和读取大文件

AIO 是 NIO 的优化,NIO 虽然是无堵塞 IO,但是其传输数据时需要确认是否有 IO 事件,而 IO 无需确认是否有 IO 事件

什么是多路复用,NIO 如何实现多路复用

单个线程监控多个客户端连接.Java 的 NIO 使用 Channel、Buffter、Selector 来实现多路复用.分为两步:

  1. 当连接建立时,连接会通过 selector 注册到 java 集合里(数组/链表/哈希表),selector 使用观察者模式监听连接是否有事件发生
  2. 客户端发送数据到达时,内核会发生 IO 中断,并通知 java 获取内核的数据,根据模型决定(select/poll/epoll)复制对应数据

模型的选择根据操作系统来决定,模型功能如下:

  • select 模模型:连接存储在默认 1024 大小的数组中,有 IO 事件发生,轮询方式查找有事件的请求
  • poll 模型:连接存储在链表中,,有 IO 事件发生,同样使用轮询获取找有事件请求
  • epoll 模型:连接存储在 HashMap 中,,有 IO 事件发生,通过内核 epll_ctl 和 epoll_wait 函数回调的信息查找哈希表对应的数据

而数据传输使用的是 Buffter 来存储数据,它是一种 Byte 数组,加入了指针来动态更新数组大小,而数据就通过 channel 通道进行传输

ByteBuffer 有什么特点

  • 零拷贝:数据之间的转移本质上不会产生新内存
  • 自动扩容:其数据结构是数组,但也可以像 ArrayList 一样进行扩容,但其扩容是按照 16 整数倍扩容,超过 512 则按 2^n 扩容
  • 内存释放:ByteBuffter 可以指定其存储在堆还是直接内存,若直接需要指定特殊方法回收,netty 使用的就是计数法,每个 bytebuffter 计数为 1,使用 handler 会为其+1,结束 handler 时需要手动-1,当为 0 时则清除内存
  • 池化管理
  • 双指针读写分离:用双指针指定了读取和写入的开始索引

Netty 用的是什么线程模型

Netty 主要用 Rector 多路复用线程模型,Rector 模型又可分为单线程 Rector、多线程 Rector 和一主多从 Rector 模型

什么是序列化和反序列化

序列化是将运行时使用的对象转为磁盘可读取的文件,反序列化反之.在 sringmvc 应用中,相应给客户端请求的对象不需要序列化,因为 srpingmvc 自带拦截器已实现了将 bean 对象转为 json 的序列化,但通过 mybatis 将对象映射给数据库时需要基础 Serializable 接口完成序列化

多线程

线程的生命周期/有几种状态

  • 新建:new Thread 创建线程后,线程会进入新建状态
  • 就绪:当调用 start()方法后,线程会先创建对应的线程内存,并获取 cpu 资源,这个过程就是就绪
  • 运行:线程逻辑运行
  • 堵塞:出于特殊原因 cpu 放弃执行该线程,堵塞分为同步堵塞(线程因锁而堵塞),等待堵塞(线程池的线程队列就是等待堵塞),其他堵塞(sleep 让线程睡眠)
  • 死亡:线程执行结束

多线程的创建方式有几种

  • 使用 Thread 类,重写 run 方法
  • 使用 Runable 接口,重写 run 方法,可以基础其他类
  • 使用 Callable 接口和 FutureTask 类,重写 call 方法,FutureTask 可以获取 call 方法返回值,但是无返回的时候会堵塞,若返回了线程则直接结束
  • 使用线程池,如 Executor,但不推荐使用 Executor 及其子类创建线程池,而是用其提供的方法 ThreadPoolExecutor 创建

为什么不推荐使用 Executor 创建线程池

Executor 其内部也是调用 ThreadPoolExecutor 创建线程池的,该类默认配置好了一些参数,方便快捷创建线程池,但是使用的线程队列是 LinkedBlockingQueue 无界堵塞队列,线程队列大小不限,会因为等待的线程可能过多而 OOM 内存溢出,且不能自定义线程名,不方便排查问题.因此推荐使用 ThreadPoolExecutor 自定义线程池,指定好对应的线程队列大小和拒绝策略

线程池的核心参数

  • corePoolSize 核心线程池:表示允许的工作线程树
  • maximumPoolSize 最大线程池:表示允许的最大线程
  • workQueue/queueCapacity 线程队列:非核心线程被保存在此
  • keepAliveTime 线程存活时间:表示非工作线程可存活的时间
  • threadFactory 线程工厂:配置创建线程的工厂对象
  • handler 拒绝策略:当最大线程池满了后,定义新线程的处理方法
  • AbortPolicy(默认):直接报错
  • CallerRunsPolicy:用执行该线程池的线程执行
  • discardPolicy:直接丢掉
  • discardOldPolicy:丢掉队列里比较老的

线程池的原理/线程池中的工作流程是怎样

这个问题要熟悉线程池的每个核心参数的作用,了解核心参数作用这题也就简单

线程池是由队列和线程构成的.当任务提交到线程池时,先判断核心线程数够不够,还有没有可以运行的线程拿来用,不够则判断线程队列满没满,没满则按照线程存活时间在线程队列排队等待,满了则判断最大线程数满没满,没满则新建线程,满了则触发拒绝策略,

线程池的核心线程数和最大线程数怎么配置合适

一般核心线程数<=最大线程数最好,=是最好,而根据业务划分,核心线程数可分为

  • cpu 密集型:技术类任务,线程数=处理器数+1
  • IO 型:处理网络连接、数据操作的任务,线程数=2*处理器数,或者线程数=(1+线程等待时间/线程执行总时间)处理器数

这些都是理论值,tomcat 默认最大线程数就是 200,实际需要通过 jumer 或者 postman 进行压测来修改

线程池有几种状态

  • Running:线程池正常工作,运行完后进入 TidYing
  • ShutDown:调用 shutdown()方法,会使线程池停止接收新线程,但线程队列里的线程正常排队运行,运行完后 ShutDown 进入 TidYing 状态
  • Stop:调用 shutdownnow()方法,会使整个线程池停止
  • TidYing:线程池没有线程运行时进入此状态,并调用 terminated()方法,该方法是空方法,可重写
  • Terminated:若重写 terminated()方法,在执行 terminated()方法时进入该状态

为什么线程池创建新线程前需要先把线程放进队列等待

创建线程会浪费资源和时间,而核心线程是可以复用的,所有会把新任务防止线程队列等待核心线程,等不下去了才会新建线程.这样设计可以提高相应速度和避免资源浪费

线程池是怎么回收线程的

核心线程默认不会回收,非核心线程会回收.当线程队列满而最大线程数没满的时候,会创建非核心线程,当线程队列为空的时候,会调用 pull 方法销毁非核心线程

你认识哪些锁

  • synchronized
  • Reentranlock(不是 lock,两者不一样)

什么是 CAS,它有什么优缺点,怎么避免这些问题

CAS 是一种乐观的并发控制机制.它由内存值、预期值和新值构成.当 CAS 开始前,会先把内存值复制给预期值,然后 CAS 开始,内存值和预期值进行比较,相同 CPU 会内存值更新未新值,不相同则不进行任何操作

优点:

  • 原子性:通过其同个 CPU 指令来进行更新,不可中断
  • 乐观锁:内存值与预期值进行比较,避免同时更新导致并发问题

缺点:

  • 由于其是 CPU 操作,频繁使用 CAS 会带来性能开销.可以用分段或记时方式判断
  • 会遇到 ABA 问题,CAS 通过乐观锁避免多线程同时修改,但是又可能多线程同时修改后又修改回来了,即 A->B->A.这种情况 CAS 无法通过预期值和内存值比较察觉.解决方法是在更新数据后添加更新版本,每次预期值内存值校验都校验以下版本信息

乐观锁悲观锁的区别

  • 乐观锁:乐观的认为更新数据前后不会或少量被其他线程干扰.因此不会引起线程堵塞,更新数据前会判断数据是否被更新过,没有则更新,有则更新失败
  • 悲观锁:悲观的认为更新数据前后一定会被其他线程干扰,通过线程堵塞来避免更新数据前后被其他线程干扰

什么是 AQS/AQS 的原理

AQS 是各种锁和同步器的父类,它主要提供了volatile 的锁状态 state、双向同步队列、单向等待队列来实现线程的堵塞操作

AQS 的工作流程主要分为 3 个阶段

  • 获取锁:获取锁会尝试获取锁,满足条件则获取成功,不满足则让把线程添加进队列,若线程是头节点,还会再尝试获取锁,成功则取出头结点,然后用自旋方式再尝试获取,获取失败就退出自旋并让线程等待,获取成功则取出节点
  • 执行线程:执行过程中若线程堵塞,会让线程进入等待队列,并释放锁,唤醒同步队列的其他线程,若被唤醒则判断同步队列尾部有无数据,有则放入尾部,没有则获取锁
  • 释放锁:释放锁会尝试释放锁,同样模板模式不提供尝试释放锁的逻辑,需要子类实现,不满足则不释放,满足则将同步队列的队列取出节点

取出节点方式分为独占模式和共享模式,因此获取锁和释放锁又有获取独占锁、获取共享锁、释放独占锁、释放共享锁

  • 独占模式:取出同步队列节点时只取头结点
  • 共享模式:取出同步队列节点时会用传播机制取出所有节点

AQS 是允许同步等待的线程中断等待的,默认是不中断模式,可以通过中断获取锁的方式将线程放入同步队列,若同步队列的线程需要中断等待,不会直接移出队列,而是触发 cancel 方法(可重写),然后把前节点 next 引用到后节点,避免其被唤醒,当后节点发生唤醒、阻塞、中断等一系列改变节点操作时,才会把中断节点移出

AQS 如何防止并发问题

  • 3 次获取锁:第一次获取锁决定是否放入队列,第二次获取锁是在加入队列后判断若是头节点则尝试获取锁决定是否堵塞,第三次是完成堵塞和加入队列操作后再尝试获取锁
  • 判断头节点状态:线程插入同步队列时,自旋判断上一个节点是否为空,为空则初始化头结点
  • CMS 更新节点和锁状态
  • 节点中断不直接移出队列,由于移出节点会受多线程影响,所以只改变其前的 next,若后节点发生堵塞、中断和唤醒等一些列操作需要改变节点时,才移出中断节点

synchronized 和 Reetranlock 的区别

  • synchronized 是关键字,而 lock 是接口
  • synchronized 遇到异常会自动释放锁,而 lock 需要用 try...cath..finaly 捕捉异常才能释放
  • synchronized 只支持独占锁.lock 支持公平锁和非公平锁
  • synchronized 锁的是对象,而 lock 是根据父类 aqs 为代码块加入 int 类型 state 标识,被锁就+1,解锁就-1
  • synchronized 没有可重入性,而 lock 有可重入性,可嵌套重复使用
  • synchronized 会自动加锁释放锁升级锁,lock 需要手动

讲讲 volatile 原理

volatile(用来实现共享变量):保证可见性和有序性.告知 jvm对象被保存在主内存而不是线程内存,并且对象创建前后被使用 lock 汇编指令锁住,lock 指令会根据 cpu 型号使用总线锁或缓存锁,总线锁是保证指导内存只能和一个线程通信,缓存锁是总线锁的优化,总线锁把整个 cpu 内存锁住,影响效率,缓存锁只锁 cpu3 级缓存中的第三级目标数据,且使用 MESI 协议和内存屏障完成.由于内存屏障所以 volatile不会触发指令重排,从而实现有序性

cpu三级缓存:L1存核心频繁使用的指令和数据、L2存核心不频繁使用的指令和数据,L3存多核共享的指令和数据
MESI协议:想要操作总线锁或缓存锁数据必须是MESI协议,MESI是cpu内存的4个状态:修改、共享、独占、失效
内存屏障:为了优化MESI协议,cpu使用storeBuffter,但这会导致由管理的数据乱序,所有cpu使用内存屏障

讲讲 synchronized 的原理

synchronized 是一种 jvm 锁,它可以作用在代码块、方法和静态方法中,它会获取类对象的对象头判断是否加锁,用 Monitor 字节码指令将程序锁住,并将 Monitor 的计数器+1,synchronized 的锁是可升级的,过程分为 4 步

  • 无锁:当 jvm 判断这段程序不会出现多线程竞争,那么其就不会加锁
  • 偏向锁:当有线程竞争程序,会由无锁升级到偏向锁,这时会用 Monitor 锁住,如果新线程过来,会判断是否为偏向锁的线程,是则放其运行(可重入),不是则升级自旋锁
  • 轻量级锁/自旋锁:新线程执行程序时发现当前程序被其他线程占用,其会原地循环 10 此获取程序执行权,若 10 次后无法获取,则升级为重量级锁.jdk7 以后自旋锁有了优化,第一次自旋 10 次失败则下次自旋次数-1,反之自旋成功下次自旋次数+1
  • 重量级锁:新线程自旋失败后进入重量级锁,线程会直接等待直到程序不被占用为止

讲讲 Reentranlock 的原理

Reentranlock 使用 CAS 方法和 AQS 类来实现加锁机制

  • CAS 方法:原子性的更新数据
  • AQS 类:一种队列同步器,内部有一个先进后出队列用来存储线程,被存储的线程会被堵塞,触发唤醒函数后会队首线程取出

而 Reentranlock 设计了两种锁,即非公平锁和公平锁,可在构造函数中使用

  • 非公平锁:新线程遇到 lock()方法,优先尝试获取锁,发现锁的信息 state 为 0(未锁),则将当前线程独占,不允许其他线程使用,并调用 CAS 方法将锁信息 state+1,此时若有一个新线程遇到 lock(),优先尝试获取锁,发现锁的信息不是 0(锁住了),则再判断独占的线程是否是当前的新线程,是的话调用 CAS 方法将锁信息 state+1,不是会把线程放入 AQS 并线程等待,再调用 CAS 方法将锁信息 state+1
  • 公平锁:新线程遇到 lock()方法,优先尝试判断 AQS 首节点是否未 null,若为 null 则说明没有线程等待,则获取锁信息,发现锁的信息不是 0(锁住了),则再判断独占的线程是否是当前的新线程,是的话调用 CAS 方法将锁信息 state+1,不是会把线程放入 AQS 并线程等待,再调用 CAS 方法将锁信息 state+1

​ 在 AQS 将线程存入的过程中,可以配置等待的线程是否可打断,默认不可打断,若配置可打断,则不想等待的线程会在队列中将节点删掉,然后执行 cancel()方法,locktry()就是重写了这一步,设计了计数器,若线程超过时间还在等待则直接打断执行 cancel 方法()

说说 AQS 的是共享模式和独占模式的区别

  • 独占模式:一个锁只允许同时被一个线程获取,reentranlock 用判断 state 是否为 0 来实现,为 0 则获取锁,不为 0 则等待
  • 共享模式:一个锁同一时间可以被多个线程获取,用判断 state 是否大于 0 来判断,大于 0 则获取锁,为 0 则等待

共享锁就是共享模式做的,假设共享锁只允许 3 个线程同时允许,那么前 3 个线程碰到锁都可以执行,而后 3 个只能等待,当它们需要被唤醒时,若用独占模式,只能唤醒一个,而共享可以把等待的 3 个同时唤醒,从而实现共享锁

AQS 队列是怎么插入的

使用尾插法将新的节点插入,但不同的是其插入前会先检查上一个节点是否为空,这时因为AQS 在插入节点前才会初始化队列,如果不检查会出现并发问题,即可能还没初始化就插入,所以需要插入前检查是否为空,AQS 检查的方式是不断自旋,若为空则初始化,不为就插入

你认识哪些同步器/你认识哪些并发工具/并发工具类的应用场景

  • CountDownLauch:一等多,考试,10 人都交卷后 1 个老师批卷
  • CyclicBarrier:多等多,LOL10 个人等待彼此加载完后才打开各自的游戏
  • Samaphore

讲讲闭锁 CountDownLauch 原理

CountDownLauch 闭锁是需要手动指定计数(解锁)次数,当调用 countdown.await 方法后,线程会堵塞,直到其他线程调用指定次数的 countDown 才会释放

其原理类似 ReentranLock,内部是共享锁机制,区别与在创建 CoutDownLauch 后会把锁信息的 state 初始化为指定值,计数(解锁)1 次-1,直到为 0 会直接唤醒 AQS 所有排队线程,不像 Reentranlock 只唤醒前面 1 个

讲讲闭锁 CyclicBarrier 原理

在构造函数中指定计数,线程都执行到 await 后等待并开始计数+1,直到所有线程都执行到 await 后在释放所有线程,重新计数

原理是通过使用ReentranLock来实现的,当第一个线程执行到 await 后开始计数,并用锁执行一个计数的任务,其他线程到了以后碰到锁都会计数并等待,直到计数等于构造函数指定的计数后,释放所有线程,并重新计数

讲讲信号量 Samaphore 原理

Samaphore 是和 CountDownLauch 反着来,前者是计数(解锁)一定次数释放锁,信号量是计数(加锁),一定次数后堵塞线程

countdownlunch 和 cyclicbarrier 的区别

  • 底层机制:countdownlauch 使用的是 AQS 的共享机制实现,cyclicbarrier 使用的是 ReentranLock 独占锁实现的
  • 工作原理:countdownlauch 是一批线程等待另一批线程为其释放,cyclicbarrier 是线程彼此等待一起释放
  • 计数量:countdownlauch 计数随意,cyclicbarrier 计数量等于线程量
  • 重用性:countdownlauch 是一次性的,释放了线程就结束了,cyclicbarrier 可重用,释放了线程重新计数

synchronized 和 volatile 的区别

讲这个问题需要先了解并发编程的 3 个特性

  • 原子性:操作时要么一次执行完成,要么不执行,不允许中断暂停
  • 可见性:一个变量可以被所有线程同时访问,不允许其他线程自己缓存起来偷偷用
  • 有序性:字节码指令串行化执行,谁先执行谁就运行,按顺序执行,jvm 为了优化编译会进行指令重排,导致无序

了解了并发编程 3 大特性再去看这两个的区别

  • 原子性:volatile 不能保证原子性,执行++操作由于在局部变量表中执行,可保证原子性,但其他多线程操作时就不能保证,而 sychronized 是排他锁,代码不能被打断,因此具有原子性
  • 可见性:synchronized 通过 jvm 指令把操作的数据放在主内存,从而实现可见性.volatile 是告知 jvm 对象在主内存而非线程内存,并加 lock 指令实现总线锁和缓存锁来实现可见性
  • 有序性:synchronized 代码块内的代码是串行化,也可以在 synchronized 里面执行重排序来打破有序性,但默认是有序性的.volatile 直接禁止指令重排而实现有序性

为什么 notify 和 wait 方法必须在 synchronized 里使用

wait 和 notify 是用来做多线程通信协调的,调用 wait 方法会将线程的锁释放再让线程在等待区堵塞,调用 notify 方法线程会尝试重写获取锁

wait 和 sleep 的区别

  • wait 执行释放线程的锁,并让线程在等待区堵塞直到重新被唤醒
  • sleep 单纯的堵塞线程

为什么不推荐使用 stop 停止线程/如何优雅的停止线程

java 官方不推荐使用 stop()停止线程,因为其本质是暴力停止线程,会把 jvm 的锁也就是 synchronized 的锁释放掉,造成安全问题,但 Reentranlock 的锁不会释放,因为其是 api 层面的锁,且若直接暴力暂停,使用了 ThreadLocal 的对象可能不会被释放,造成内存泄漏问题

推荐使用 interrupt()方法中断线程,使用 Interupt()方法后,然后判断 if(Thread.currentThread().isInterrupted())结果执行自己的逻辑手动结束线程,这样可以自定义锁的释放问题,也可以解决 ThreadLocal 导致的内存泄漏问题

什么是 ThreadLocal?它的原理是什么

ThreadLocal 是用来存储每个线程的变量,创建好 ThreadLocal 后,每个线程都可以在里面设置属于自己专属变量,其他线程无法干扰.其底层是通过使用 ThreadLocalMap 实现

原理是把变量放在具体的线程空间上

ThreadLocal 什么情况下会产生内存泄漏

ThreadLocal 的变量属于弱引用,当数据为 null 时或线程销毁时会被回收,但是如果碰到线程池,数据不为空且不会被销毁,则不会回收,产生内存泄漏.

因此在使用完 ThreadLocal 后要用 xx.remove 手动情况里面的数据为 null

多线程如何进行线程通信/ThreadLocal 如何传递数据给子线程

子线程传递数据给父线程,只需修改父线程的 ThreadLocal 即可
父线程不能直接传递值给子线程,需要使用 InheritableThreadLocal,它会把 ThreadLocalMap 里的数据复制给子线程

什么是死锁/如何避免死锁

两个以上的线程互相抢夺资源而产生的循环等待问题被称为死锁,死锁产生的条件有:

  • 一个资源只能被一个线程使用
  • 线程等待时不释放资源
  • 线程在使用某资源时,其他线程不能申请该资源
  • 若干线程首位相接

打破死锁的方式就是打破上述的 4 个条件其中一个就可以

  • 设置共享锁,比如 ReadLock 就是共享锁,而独占锁如 ReetranLock 会出现一个资源一个线程使用的问题,但是共享锁要考虑线程安全,只是读取数据没有线程安全问题,但是如果有写数据就不能使用共享锁
  • 设置线程等待超时处理方案,比如使用 locktry()
  • 若不能申请资源,则释放线程而不让其等待
  • 设置锁的时候注意加锁顺序

串行、并行、并发的区别

  • 串行:程序按顺序一个个执行
  • 并行:程序同时执行
  • 并发:程序看似同时执行,实际底层是任务被拆分成多个,cpu 交替执行

什么是守护线程

线程分为守护线程和用户线程,用户线程就是普通的线程,守护线程会在所有普通线程关闭时才自动关闭,GC 垃圾回收器就是在守护先里,也可以手动配置一个线程变为守护线程

说说你对线程安全的理解

线程安全是指多线程在执行相同代码时,得到正确结果,那么这个线程就是安全的.如果一个线程在执行操作的时候因其他线程影响得到的最终结果不正确,那么就是线程不安全

使用了 ConcurrentHashMap 一定线程安全吗

不一定,ConcurrentHashMap 虽然使用了锁,但是其 Put 和 get 方法是原子性的操作,如果在多线程情况下,每个线程都复合使用 put 和 get 这样的原子性操作还是会出现数据不一致的问题

举例:A 线程尝试 get 数据,hashmap 上锁保护,get 完成后锁就解开了,然后 A 线程开始尝试根据 get 的值 put 数据,但是 B 线程也开始 get 数据,这两个原子性操作就会出现竞争这把锁,如果 A 线程竞争到了还好,竞争不到 B 线程可能会拿到未修改的数据,这时 A 再修改或者 B 再修改都是错误的

解决方法要具体业务具体分析,上一个例子可以用 volatile 用共享变量解决,或者把整个原子性的复合操作加锁

线程通信的方式有哪些

  • wait 和 notify 设计线程通信
  • 使用共享变量 volater
  • 使用 Juc 包工具 AQS 的子类或 exchanger
  • 使用 rxjava

JVM

JVM 的主要功能是什么

  • 解释和运行:在虚拟机中解释和编译字节码文件,实现跨平台运行
  • 内存管理:为对象自动分配内存,自动回收不再使用的对象
  • 即时编译:java 中对常用的热点关键字代码进行缓存,下次使用的时候不再通过编译而是直接读取缓存

JVM 是由那几部分构成

  • 类加载器:用来加载字节码文件
  • 内存管理区:用来存储加载的类和接口
  • 执行引擎:包括即使编译、解释器和垃圾回收器
  • 本地接口:由于 jvm 底层也是 c++制作,所以内部含有 c++接口

1 个 class 字节码文件由哪几部分构成

  • 基本信息:字节码对应的 java 版本号、访问标识符等
  • 字段:对象的名字、类型
  • 常量池:包括常量池(这个常量池只是避免字节码文件出现重复的字节,不是字符串常量池)、类或接口名等
  • 方法:类的方法信息,包括字节码指令
  • 属性:类的内部类列表

i=0;i=i++;i 应该是多少

  • 简单回答:0
  • 高级回答:i++会先把 i 存放再临时操作数栈中,然后对局部变量数组中的进行+1 操作,这时 i 会变成 1,但是临时操作数栈中的 i 会覆盖局部变量数组

解释:

  • =产生的字节码指令: 会把局部变量数组中的对象数据放入临时操作数栈,执行完=右边的操作后会放回局部变量数组
  • ++产生的字节码指令:会直接修改局部变量数组里的值,而不是像其他运算符修改临时操作数栈的值

简述类的生命周期以及流程

类的生命周期大致可分为 5 个阶段,也可以说是 7 个阶段

  • 加载
  • 连接(验证、准备、解析)
  • 初始化
  • 使用
  • 卸载

在类的加载阶段,会把字节码文件信息的 5 个信息(基本信息、常量池、字段、方法、属性)加上一个虚方法表(记录父类的父方法)加载到方法区.其中方法区在 jdk8 以前是永久代,jdk8 以后,字段和方法这两个属性被加载到堆区,而方法区在 jdk8 以后其他 4 个数据从永久代改到元空间

在连接阶段,会把存储在内存中的字节码文件进行校验,查看字节码是否符合虚拟机规范(如前前几个字节含"caffee"这个字段).然后会将 static 的变量进行初始化默认值(只是默认值,不执行赋值操作,除非 static 有 final 修饰),最后解析将字节码常量池信息由编号改为内存地址(字节码文件中出现的常量由编号指向,类似一对多,解析后由内存地址指向)

初始化阶段,会执行静态代码块 static{}中的语句
只有用上类级别的对象或方法才会进行初始化
以下情况会触发初始化阶段:

  • 直接获取静态对象:类.静态对象会触发 static{}
  • new 类会触发 static{}
  • 执行当前类的 main 方法会触发 static{}
  • 调用 Class.forName(类名)会触发 static{}

以下情况不会触发初始化阶段:

  • 获取的静态对象被 final 修饰,final 在连接阶段就已完成初始化
  • new 的是一个数组,new 类[10]不会触发 static{}
  • 掉用的是父类的静态对象,不会触发子类的 static{},如子类.父类静态对象
  • 以及被初始化过的类,不能再被初始化

因此,初始化阶段的执行顺序是静态代码块->代码块->构造函数->main

类的加载器有哪些

  • bootstrap 启动类加载器:用来加载 java 核心类(加载目录 jre/lib)
  • extension 拓展类加载器:用来加载 java 官方提供的通用但非核心的类(加载目录 jre/lib/ext)
  • application 应用类加载器:用来加载自定义的类(项目目录/target/classes)

什么是双亲委派机制

为防止一个类重复出现被多个类加载器加载,jvm 引入双亲委派机制,其通过自下而上首先从 application 应用类加载器->extension 拓展类加载器->bootsrap 启动器类加载器去查询该类是否被加载,若被加载则返回类的对象,若没被加载,就自上而下从 bootsrap 启动器类加载器->extension 拓展类加载器->application 应用类加载器去查找该类是否在该启动器的加载目录,找到就加载

如何打破双亲委派机制

  • 自定义类加载器:通过集成 ClassLoader 这个抽象类,实现 loaderClss 方法来实
  • 线程上下文获取类加载器:通过线程上下文获取类加载器 getContextClassLoader()类加载器,从而避开由父加载器委派,属于打破双亲委派.
  • osgi 机制,此方法已被淘汰,但提供了热部署思路:本质上还是自定义类加载器,一个类加载器委派给另一个类加载器,从而实现线上热部署

JDBC 如何加载驱动,是否打破双亲委派

JDCB 是否打破双亲委派是有争议的说法,JDBC 使用的是 jdk 的一种 SPI 服务发现机制(将接口实现类的名字配置在文件中,并由服务加载器读取配置文件,加载实现类)即由启动类加载器加载 Driver 服务加载类,通过此类来查找项目下的驱动类,找到后使用线程上下文获取类加载器 application 应用加载器来加载.

TomCat 如何打破双亲委派机制

每个 webapps 应用都有一个自定义类加载器

Spring 如何打破双亲委派机制

讲讲 java 的内存结构

  • 堆(线程共享):jdk8 以前存方法区,jdk8 以后存方法区里的字段、对象和字符串常量池,堆内存又可分为 used(已使用的内存)、total(总共内存)、max(最大内存),当 total 不够用时会自动扩张,直到 max 最大内存
  • 方法区(线程共享):jdk8 以前方法区在堆的永久代里,jdk8 以后在本地内存里,且把方法区存的类基本数据、常量池、字段、方法、属性中的字段和方法存进堆里,把常量池分为运行时常量池和字符串常量池,字符串常量池也放进堆里,方法区存字符串常量池的引用,把方法区存的命名空间由永久代改为元空间
  • 栈(线程不共享):分为虚拟机栈和本地方法栈,如果由 navitide 修饰就用本地方法栈,它是用 c++实现的,没有就用虚拟机栈,栈存栈帧,每个方法都是以栈帧的形式存入,栈帧又分为局部变量表(存基本数据)、临时操作数栈(存需要计算的数)、帧数据(存动态链接,类解析后的内存引用链接、存方法出口,方法返回的位置和类型、存异常表引用,trycatch 的引用位置)
  • 程序计数器(线程不共享):存每个线程执行到的字节码指令位置,以便切换线程时指导上一次执行到了哪
  • 本地内存(线程共享):jdk8 以后存元空间,但除此之外,如果有用上 NIO 或文件操作技术如(BuffterWrite),那么这些对象会被存在本地内存.因为如果不存本地内存,那么执行顺序是读取数据->本地内存->堆->读取堆数据,这样效率太低,不如读取数据->本地内存->读取本地内存数据,另外本地内存的对象不属于 jvm,所以垃圾回收不会回收到,需要手动销毁

讲讲各个内存是怎么回收的

  • 线程不共享的内存会随着线程销毁而内存销毁
  • 线程共享使用垃圾回收器回收:主要通过判断是否被引用和引用类型,方法区的类对象还要额外判断类的对象或子类是否被回收才能回收

GC 如何判断对象被引用

  • 引用计数法:jvm1.3 之前,一个对象被创建的时候同时会创建其标数,被使用则+1,当标数为 0 时则回收

  • 可达性分析法(目前使用的方法):栈区数据不和普通对象成引用关系,而是栈区数据和 gcroot 对象成引用关系,普通对象再和 gcroot 对象成引用关系,若普通对象无法链路到 gcroot,则可回收,gcroot 是不可回收对象.

一下对象可称为 gcroot 对象:

  • 每个线程 Thread 对象
  • 通过类加载器加载出来的类对象,静态变量的 gcroot 是它
  • synchronized(这里的对象),锁的 gcroot 是它
  • 本地方法调用的全局对象(jvm 本地接口调用 c++的对象 )

什么是强、软、弱、虚引用

  • 强引用:如 new Object();这样创建出来的对象是强引用,无法被 gc 回收,当内存不足时会抛出 OOM 异常
  • 软引用:由 new SoftReference<类型>(对象)实现,空间不足时回收
  • 弱引用:由 new WeekReference<类型>(对象)实现,被 gc 发现则回收
  • 虚引用:不能单独存在,必须和软/弱引用对象联合使用,当对象被垃圾回收器回收时,执行特点方法
  • 终结器引用(面试可用来吹牛,实际不会用):Object 有一个方法是 finalize(),对象使用 finalize()时,GC 第一次回收不会回收它,第二次才会回收它

拓展:假设软/弱的对象已被回收,如何回收软/弱引用对象?这里以软引用为例,使用引用队列 ReferenceQueue,在 new SoftReference<类型>(对象,引用队列),当软引用内部对象被回收时,就可以通过 queue.poll()获取到这个软引用,再使用软引用对象 = null;回收它

介绍一下你了解的垃圾回收算法

垃圾回收算法主要分为:

  • 标记清除算法:根据可达性分析算法获取可回收的对象和不可回收的对象,将它们进行标记,GC 执行时清除标记可回收的对象
  • 复制算法:将内存空间分为一半,一半存当前堆的对象,当 GC 执行时,将不可回收的对象复制到另一半,清除掉可回收的那一半
  • 标记整理算法:对标记整理算法的优化,清除对象后重新整理内存空间,避免出现内存碎片
  • GC 分代算法(此算法是最常用的算法,它严格来说不是一种算法,而是一种思想,将标记整理和复制算法进行了结合):该算法把堆区 used 区域分为新生代和老年代,新生代使用复制算法,其通过再细分将新生代分为伊甸园区和两个幸存者区 S1、S2.新创建的对象会被出在伊甸园区,当伊甸园区满了后会触发新生代的垃圾回收 YoungGC(或者叫 MinnorGC),YoungGC 会把伊甸园区数据放在 S0,然后使用复制算法进行回收,不同的是当复制不可回收对象时,会对不可对象进行+1 标记,当标记到达 15 时,会把不可回收对象放在老年代.当老年代也满了以后,会先触发 YoungGC 再触发 FullGC,FullGC 是对整个堆区进行回收,新生代用复制算法,老年代用标记整理算法清除.

JVM 性能如何调优

JVM 性能一般不需要调优,如果条件艰苦或项目需求不同,可以考虑

  • 根据业务需求调整:若一个服务属于频繁处理请求对象,没有或很少保存长期对象,那么就可以考虑减少老年代空间,反之,若一个服务很少有新对象产生,而保存了很多 Spring 的 Bean 对象,可以减少新生代,扩充老年代.

  • 通过查看 FullGC 次数、YoungGC 次数调整,若 YongGC 频繁触发,说明线上服务可能工作频繁,可以考虑增加新生代或者查看 QPS 请求数判断是否被攻击.若 FullGC 频繁触发,可以考虑增加老年代空间,或者跟踪代码查看是否死循环

JVM 垃圾回收器有哪些

  • Serial 和 SerialOld:前者是单线程的新生代的复制算法回收器,后者是单线程的老年代的标记整理算法回收器
  • ParNew 和 CMS:前者是多线程的新生代复制算法回收器,后者是使用 4 步:初始标记(标记 GCrood 对象)->并发标记->重新标记->并发清理,使用的是老年代的标记清除算法,它的重新标记是防止并发标记时产生新的对象没有标记到.
  • ParallerScavenge 和 ParallerOld(简称 PS 和 PO):前者是多线程的复制算法回收器,不同 ParNew 的是它可以自动调整堆大小,并允许设置最大暂停时间,是 jdk8 默认回收器.后者是多线程的老年代标记整理算法回收器
  • G1:jdk9 默认的垃圾回收器.jdk9 以前的垃圾回收器老年代和新生代是连续的,它的特点有以下:
  • 分块处理:G1 将老年代和新生代的每个区拆分为独立且无个数限制的块(只要内存足够大),要求逻辑连续物理不连续.这样垃圾回收的时候不必全堆扫描,只用找到对应的块.
  • 设置最大处理:G1 允许设置最大暂停时间,若有设置最大暂停时间,则同一时间只能处理特定的某个块
  • 新生代新处理方法:在处理新生代时,当伊甸园区块占 60%的时候,会触发 YoungGC,把伊甸园区数据放进幸存者区或 Humougous 区,使用复制算法,当伊甸园区要放进幸存者区的对象超过幸存者区的一半,则直接放进 Humougous 区,Humougous 区属于老年代,但也被独立为一个块
  • 老年代新处理方法:在处理老年代时,若老年代区块占 45%,则触发混合回收,回收所有新生代和部分老年代,它使用的是复制算法,具体步骤类似 CMS,分为初始标记(标记 GCrood 对象)->并发标记->最终标记->并发复制清理,它的最终标记不同 CMS 重新标记,它是标记那些并发标记中因引用改变而漏标的对象,不会处理新产生的对象.

内存溢出问题有哪些?如何解决

  • OOM 异常:程序内存不足,启动时配置内存 java xx.jre -xmx 内存大小
  • OOMGC 异常:GC 回收时内存不够
  • ....

如何解决频繁 GC 的问题

  • 使用 top 命令查询 cpu 状态
  • 区分是 fullGC(老年代 GC)问题回收 YGC(新生代 GC)问题
  • 使用 jstack 追踪线程,查看对应代码.是否死循环等问题

什么是持久代?/为什么删除持久代?/为什么增加元空间

jdk1.8 以前,持久代主要用来存放类的信息、字段、名字等元数据

但是持久代存储的数据量是有上限的,默认是 64m,超出会报异常,因此使用元空间代替

jdk1.8 后,jvm 把方法区存放在元空间里.每个类都会创建一个元空间,用来存储类的常量池、方法区、类信息元数据.元空间属于本地内存,不归 jvm 管

String 类型的变量会被 gc 回收吗

看清楚,直接"xx"的变量属于常量池,不会被回收,new String("xx")属于堆,会被回收

堆区分为哪几个结构?有何异同?

gc 回收会根据不同的对象进行划分不同区域

  • 新生代:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
  • 老年代:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。

数据库

什么是索引/索引有哪些

索引类似数组的下标,存储数据表中某一特定数据位置的数据.相对于字典的目录,可以优化数据库的查询速度

  • 按数据结构划分:B+树索引、Hash 索引、全文索引
  • 按底层物理存储划分:聚簇索引、非聚簇索引
  • 按存储内容划分:主键索引(只能有 1 个,不允许空)、唯一索引(只能有 1 个,允许空、普通索引(允许重复,允许空)
  • 按字段个数划分:单列索引(1 个索引对应 1 个字段)、组合索引(1 个索引对应多个字段,用最左前缀原则结合)、联合索引(特殊的组合索引,每个字段顺序是连续的)

索引的优缺点又哪些

优点:

  • 提高查询速度
  • 通过唯一索引能保证数据唯一
  • 加速两个表的连接

缺点:

  • 创建索引需要申请额外存储空间
  • 含有索引的字段,发生增删改时由于需要更新索引,所以效率较慢

了解哈希索引吗

哈希索引是基于哈希表来实现的,每一行数据都会生成对应的哈希码,哈希索引将所有哈希码存储在索引中,通过哈希码可以找到对应数据的指针.因此哈希索引的查询速度会更快一些,但是哈希索引由于无序特征,不能用于排序,哈希索引只支持等值查询,不支持其他范围或模糊查询,因为哈希码是整列的内容来计算的

只有 memory 引擎可以显示设置哈希索引,且其默认就是哈希索引.innodb 不支持直接设置哈希索引,但在特定情况下可以转为哈希索引:若某个辅助索引被频繁使用,会自动转到哈希索引里

聚簇索引和非聚簇索引的区别

  • 聚簇索引:索引和值都存储在叶子节点上,能提高查询速度,但涉及增删改操作时由于需要更新索引索引效率慢,聚簇索引就是主键索引.聚簇索引每个非叶子节点存的都是键
  • 非聚簇索引:用一个额外的平衡树存储索引,先查询索引,再根据索引查询值,查询速度慢,但涉及增删改操作不需要回表更新,索引增删改效率高.辅助索引就是非聚簇索引,在 innodb 中,在聚簇索引之上创建的都是辅助索引.非聚簇索引每个非叶子节点存的都是值

主键索引、唯一索引、普通索引的区别

  • 主键索引:不允许为空和重复,每个表只能有 1 个,可作为外键,主键索引就是聚簇索引
  • 唯一索引:不允许重复,可以有多个,不可作为外键
  • 普通索引:可重复,可为空

普通索引尽量用普通索引,除非要保证唯一性才需要使用唯一索引,唯一索引和普通索引在搜索和增删改上效率是不同的

搜索:B+树搜索是根据叶子节点的双链表使用二分查找搜索数据的,唯一索引找到数据就停止,普通索引会继续往下查找
更新:普通在更新数据时,如果数据页在当前内存 change buffter 中,就直接修改内存的 change buffterz,就不会重新从磁盘中修改,如果不再内存中,就在磁盘中查找,但唯一索引在内存和非内存的修改数据前会进行一次判断是否重复

单列索引和组合索引、联合索引的区别

  • 单列索引:一个索引对应一个列,如果有多个单列索引,查询优化器会选择其中一个
  • 组合索引:一个索引对应多个列,使用它的好处是减少空间开销,但是其必须符合最左前缀原则
  • 联合索引:特殊的组合索引,其字段是连续的

什么是最左前缀原则/最左前缀原则何时失效

在使用联合索引时,查询的字段必须符合最左前缀原则时才索引才会生效,具体指的是查询的字段经过排序后从左往右和联合索引的顺序相同.若查询的字段跳过联合索引的字段顺序,那么就会失效

innodb 和 myisam 的区别

  • innodb 默认使用聚簇索引创建 b+树,若没有聚簇索引(主键索引),那么其会使用唯一索引作为键值创建 b+树,若叶没有唯一索引,那么其会创建一个隐式的主键索引.而 myisam 则聚簇索引
  • innodb 支持视为,myisam 不支持事物
  • innodb 可以使用外键,myisam 不可用使用外键
  • innodb 没有保存行数,查询行数需要全文扫描,myisam 保存了行数
  • innodb 是行锁,多线程操作同行会上锁.myisam 是表锁,多线程操作同表会上锁.

B 树和 B+树的区别

  • 数据结构不同:B 树是平衡二叉树,而 B+树是平衡二叉树下面加了双向链表,无论 B 树还是 B+树,其每个节点存储的个数不能超过 2m-1,m 是阶层
  • 存储内容不同:B 树存数据页和页地址,B+树非叶子节点存页地址,叶子节点存数据页,包括页目录和数据
  • B+树查询只查询子节点,子节点之间有引用,而 B 树要查询全部
  • B+树根节点保存在内存,子节点在磁盘

sql 语句的执行过程/mysql 的结构是什么,分别有什么用

理解什么是索引下推需要直到 mysql 的结构分为:

  • 连接层:负责与客户端建立 TCP 连接
  • 服务层:负责解析 SQL 语法,对引擎层返回的数据进行筛选
  • 计算引擎层:负责存储和提取数据,innodb 使用 B+树实现,每个叶子节点都是一个数据页(16kb),数据页包含数据和页目录(将数据页数据切分,便于查询),每个非叶子节点都存储这其下一层数据页的目录.当要执行查询的时候,会先根据索引从头节点一层层找下去,最后找到其对应的数据页,再根据数据页的页目录找对其对应的目录,再从目录里遍历查询对应数据,若有范围查询,再从当前数据页直接跳到下一个数据页查询
  • 文件系统层:所有数据、日志保存与此

什么是索引下推

索引下推是一种查询优化方案,当需要查询一个有条件的数据的时候,通常情况下是计算引擎层返回查询的数据,服务层再根据条件筛选.开启索引下推后,会先在服务层筛选数据,把筛选到的索引再交给计算引擎层去提取,这样可以提高查询效率

什么是回表、什么是索引覆盖

innodb 根据非聚簇索引查询数据的时候,会先在一个平衡二叉树里根据值查询索引,查询到索引后若需要返回的是不止当前值的数据或需要返回整行数据,则需要根据查询到的索引再去聚簇索引里查询整行数据,这一步操作就是回表(查完非聚簇再去查聚簇),而索引覆盖指的是需要返回的数据就是非聚簇索引的值,那么就直接返回,不再去查非聚簇索引(查完非聚簇索引就结束)

索引什么时候会失效

七字口诀:模型数空运最快

  • 模:使用模糊查询,且查询条件以%开头(索引覆盖除外).一种特殊情况就是索引覆盖,查询的值刚好就是辅助索引的键,就不会回表查询,这样就属于使用索引了
  • 型:查询时使用的类型错误,即使有隐式转换也不会用索引
  • 数:查询的字段内部又用了函数,如 select sum(字段)
  • 空:索引字段查询条件是 is null、is not null、!=或 or,但是如果实际数据中 null 值过少,查询优化器会判断走索引
  • 运:查询的字段使用了四则运算
  • 最:联合索引,查询的字段不符合最左前缀原则.但是使用 order by 即使符合最左前缀也会失效,因为 order by 排序会判断 where 有没有用索引,如果没有 where 则 orderby 自己使用排序算法排序,返回的数据自然没有使用索引.因此 orderby 可能会导致索引失效,除非索引覆盖
  • 快:Mysql 查询优化器判断使用全文搜索比索引搜索更快就不会用索引搜索

索引失效的最根本原因就是索引查询速度大于全表查询速度,查询优化器使用全表查询

索引排序的流程?为什么索引排序可能导致索引失效

mysql 使用 order by 来执行排序,具体流程如下:

当计算引擎层将数据返回到服务层时,服务层开始判断 sql 语句有无使用索引查询,如果又使用索引就会判断是索引快还是全表扫描快,若索引快直接使用索引进行查询,返回的数据本身就是排好序的,若全表快,使用全表扫描出数据后再用自带的排序算法排序

其排序算法是先判断数据量大小是否超过 sort buffter,默认 256kb,可配置,若未超过使用快排,超过了将文件拆分为 n 个 sorf buffter,局部快排,外部归并

varchar 和 char 有什么区别/varchar 和 char 的应用场景

  • varchar 是可变长度字符,char 是不可变长度字符,若存储'abc',使用 char(10)会占用 10 个长度,剩下 7 个会用空格填补,而使用 varchar(10)就只占用 3 个.但是 varchar 效率较低,当需要更新 varchar 的数据时,每次更新 varchar 都会改变长度,而 char 不需要改变长度.
  • char 只能存 255 个字符,与编码无关.而 varchar 能存 2^16-1 个字节,而具体一个字符多少字节与其编码有关

事物的基本特性有哪些/什么是 ACID

事物的基本特性指的是 ACID,即:

  • A 原子性:一个事物操作要么全部成功,要么全部失败.在 mysql 中靠 undo log 来保证,它会记录需要回滚的日志信息
  • C 一致性:数据库从一个一致性状态变为另一个一致性状态.比如 A 有 100,B 有 0,加起来共 100,A 转账 50 给 B,但是转账失败了,两人的钱加起来应该还是 100,B 不能突然多出 50,前后应该数据应该一致.靠其他三个特性来保证
  • I 隔离性:一个事物在操作时,另一个事物是不可见的.靠 MVCC 来保证
  • D 持久性:事物一旦提交,就永远保存在数据库中.靠 redo log 来保证,它会记录每次修改的信息

binlog 和 redolog 的区别

redolog 是事物每次尝试修改前都会写入修改记录
binlog 是事物最终提交前才会写入的修改记录,通常恢复数据使用 binlog/redolog,主从同步使用 binlog

事物隔离级别分为哪些

  • 读未提交:允许读取还未修改完的数据读取的数据可能是正在修改还未修改完未提交的数据
  • 读已提交:读取到的数据必然是刚修改完提交完的数据
  • 重复读:连续读一个正在修改的数据时,不管数据修改成功提交或回滚,拿到的数据结果都一样
  • 序列化:使用类似锁结构,事物开启时不允许其他事物开启

事物问题有哪些

  • 脏读:隔离级别为读已提交时产生,事物读取了另一个事物未提交的数据,设置读已提交可解决
  • 幻读:隔离级别为幻读时产生,两个事物执行相同查询,结果不同,设置一致性非锁定锁可解决
  • 不可重复读:同一事物同一查询结果不同,设置重复读可解决

MVCC 是什么

MVCC 是数据库并发控制的一种操作,它使用乐观锁来实现,可以将多个事物进行隔离,类似多线程隔离.由于 mysql 自带的共享锁或排他锁都是悲观锁的一种,是主动加锁,这有可能导致线程堵塞,因此 MVCC 使用乐观锁来实现.

其实现原理是每当一个事物创建时,会创建一个集合 readview 用来存储未提交的事物版本号,当事物对数据表进行操作时,会通过快照读(无锁 select)查询每个表的隐藏字段(当前事物版本号,上一个事物版本号)获取事物版本号,若查询的版本号小于 readview 最小的版本号,则代表当前数据表数据已经被其他事物提交了,新事物可以正常提交,那么 readview 最小的事物版本号就可以提交.若查询的版本号大于或等于 readview 集合里的事物版本号,则代表数据表的数据正在被一个事物挂着,则使用该表的上一个历史事物版本号(innodb 默认保存 3 个历史版本,不然会带来存储开销问题)

读未提交:不使用 mvcc,因此读的数据可能就是正在修改还没修改完没提交的数据
读已提交:正常使用 mvcc,每次事物开启都创建一个 redview,读到的数据必然是已修改完已提交的数据
重复读:不正常使用 mvcc,每次事物开启都会判断是否已经创建了 readview 了,有就沿用上一个 readview,因此可能出现第一次读的数据是未提交正在修改的数据,而返回其历史版本,但是当这个数据修为完提交了,由来 1 个读事物,还会判断其为未提交正在修改数据,返回其历史版本,导致读到重复数据

MVCC的乐观锁不是CAS实现的,而是快照读(无锁select查隐藏字段)

什么是三星索引

三星索引是数据库索引查询的一种评分,三星是最理想的,但通常情况下到不了三星

  • 一星:查询多个值的时候,下一个值刚好就在旁边的叶子节点(数据页)
  • 二星:排序的时候查询的值是主,主键由于本身就是排好序的,因此不用额外排序
  • 三星:查询的数据刚好是查询的列,就是索引覆盖

一颗 B+树能存多少行数据/Innodb 能存多少行数据,怎么算的

假设 1 行数据 1kb,那么一个叶子节点(数据页)能放 16 行这样的数据

假设主键使用 bigint,占 8 个字节,innodb 指针占 6 字节,一个数据就占:6+8=14 个字节

一个非子节点能存 16kb 数据,那么一个数据页能存 16kb/14b = 1170 个,因此一个数据页能存大概 1170 个数据

因此,若 b+树是 2 层,那么其存储的数据量为 1170*16=约 2 万,若为 3 层,那么其存储的数据量是 1170*1170*16 = 约 2 千万,若是 4 层,其存储的就是 1170*1170*1170*16=200 亿

mysql 的锁有哪些

  • 根据锁属性划分:全局锁、共享锁、排他锁
  • 根据锁的力度:表锁、行锁、记录锁、间隙锁、临建锁

什么是全局锁、共享锁、排他锁

  • 全局锁:对整个数据库实例加锁,使整个数据库变为只读状态
  • 共享锁:又名读锁,允许多线程读取数据库内容,但是不允许其对数据内容进行修改,直到共享锁释放
  • 排他锁:又名写锁,当对某数据进行更新的时候,不允许其他线程对该数据进行更新,但可以读取

什么是表锁、行锁、记录锁、间隙锁、临建锁

  • 表锁:当事物对数据库进行操作时,事物对整个表进行上锁,在该事物未结束前其他事物不能访问,myisam 默认使用
  • 行锁:当事物对数据库进行操作时,事物对操作的行进行上锁,在该事物未结束前其他事物不能访问,innodb 默认使用
  • 记录锁:特殊的行锁,只锁住一行,一般出现在唯一索引
  • 间隙锁:特殊的行锁,锁住一个区间,此区间是左开右闭(n,m]
  • 临建锁:特殊的行锁,锁住一个区间,此区间是左闭右闭[n,m]

Mysql 有死锁吗?怎么避免死锁

死锁是指两个以上的线程抢夺资源而互相等待的问题

两个线程 AB 同时对两行数据进行修改的时候会出现死锁,比如 A 线程修改第一行和第二行,B 线程修改第二行和第一行,就会出现互相等待

解决方法就是开启超时回滚就行,默认超时回滚时间就是 60s,或者在业务层方向将多个修改整合为 1 个修改,进行批量修改

mysql 为什么需要主从同步

  • 数据备份,防止宕机
  • 任务分配,Master 负责读,Slave 负责存

mysql 复制原理是什么

使用 binlog 二级制日志文件记录数据的改变,在复制的时候就会读取 Binlog 文件

数据库 ID 生成有哪些策略

  • 自增
  • uuid
  • redis 自增:使用内存,性能好,但是其也是自增的一种,而且容易数据泄露,重启系统后可能会生成同样的 id
  • 雪花算法:由 1 位占位符、41 位时间戳、10 位机器 id、12 位随机序列号生成 id

分布式锁如何实现

  • 使用数据库,当线程被锁上在数据库记录,其他线程想要执行先访问数据库查询是否被锁
  • 使用 zookerper 的临时节点作为锁
  • 使用 redis,查询某 key 是否存在,存在则等待,执行时添加上 key
  • 基于 redis 方案生成的 redssion 框架

redis 分布式锁需要使用什么命令

setnx key value ex 时间,使用 setnx 代表若 key 不存在则无法创建,添加过期时间是为了防止死锁

zookerper 和 redis 做分布式锁有何区别

  • zookerper 做分布式锁,每次上锁都要创建几个临时节点,使用完锁自动删除
  • redis 只是简单的对数据进行操作

限流算法有哪些

  • 计数器算法:每个一段时间设置请求上限
  • 滑动时间窗口算法:计数器算法的滑动版
  • 漏桶限流算法:规定最大请求上限
  • 令牌桶限流算法:按一定速率生成令牌存入桶内直到上限,请求过来时从桶里取令牌才能通过,无令牌则等待

微服务开发的原则是什么

  • 每个功能就是一个独立的服务

什么是 CAP 定理

  • C:数据一致性
  • A:服务可用性
  • P:分区容错性(指服务可独立对外,A 服务宕机不影响 B 服务)

什么是 BASE 理论

  • B:基本可用性(允许宕机时服务损失部分功能,保留关键功能)
  • S:软状态(允许数据在同步时存在延迟,同步的数据处于中间状态)
  • E:数据最终一致性(无论同步是否延迟,最终数据都必须一致)

什么是 2pc 协议

2pc 协议是分布式事物的一种处理方案,当多个服务需要提交事物时,需要一种协议来处理,2pc 协议是其中一种,其步骤分为

  • 第一阶段:各参与者提交操作,协调者向参与者发送请求是否同意,成功提交操作代表同意
  • 第二阶段:若参与者都同意,则协调者将事物全部提交
  • 第二阶段:若有一个参与者不同意,则回滚所有事物

2pc 协议有什么缺点

  • 堵塞问题:第一阶段只提交 sql,不执行事物,占用资源
  • 单点问题:协调者宕机会导致参与者堵塞

什么是 3pc 协议

3pc 在 2pc 基础上加了一个阶段,在各参与者提交操作之前先向协调者提交状态,协调者确认状态后才能提交操作

什么是 TCC 协议

TCC 协议是当前分布式事物最常用的协议,其优点在与无堵塞,随时回滚

  • T:协调者确认参与者事物状态
  • C:若参与者正常,协调者同意参与者提交事物
  • C:若参与者宕机,协调者回滚所有事物

对比 2pc,好处在于 2pc 在其他参与者未同意之前都不会提交事物,而 tcc 会直接提交事物,出错了再回滚,这样避免了数据库连接池堵塞

为什么 TCC 会出现空回滚

空回滚是因为协调者再 T 阶段确认参与者状态时,由于网络原因等问题无法获取其状态,就会空回滚

如何解决 TCC 幂等/悬挂问题

幂等问题:sql 修改数据库时,事物提交但是提交失败,重新提交时数据不一致.tcc 协议在 T 阶段确认完毕后会执行其中一个 C 阶段,但是可能由于网络问题导致已经提交事物了,但是没有反馈提交成功,tcc 以为没有成功所有再提交异常

悬挂问题:由于网络延迟,协调者再向参与者发送 t 确认时超时,而参与者再收到 t 以后仍然执行确认事物

解决方法:使用数据库记录协调者是否提交

有哪些消息服务方案

看具体业务需求:

  • 同步:微服务使用 feign 的 http 协议调用,dubbo 使用 rmi 协议远程调用方法
  • 异步:使用 MQ 异步调用

最大努力通知方案是什么

若想要将某消息发送给某服务,需要最大努力保证接收方会收到,有一下方案可以处理:

  • 发送方重复发送,直到接收方返回接收成功
  • 接收方接收到数据后对数据进行确认

什么是幂等问题?有什么常见解决方法

幂等问题就是重复执行同意任务

  • 数据库记录:当执行某操作时,向数据库查询是否有执行过该操作
  • token 令牌:执行某操作时携带令牌,执行完后删除令牌

什么是双写一致性问题

如何保证 redis 和 mysql 数据一致,就是更新数据,是先更新/删除缓存好,还是先更新/删除数据库好,有两种方案:

  • 保证强一致性:更新数据前加上锁,保证无第二线程影响,更新数据库,再删除 redis(有性能影响)
  • 保证最终一致性:使用延时双删策略,更新数据前先删除缓存,再更新数据库,更新完成后再借助 MQ 发送异步延时删除 redis(可以使用阿里的 cannel 监听 binlog,有更新则触发 MQ 发布延时删除),若删除失败则再发布 MQ 再删除

通常使用:先更新数据库,再删除缓存

我的方案:更新数据库前加上 redis 锁,更新完后解锁,在删除数据库前判断是否有 redis 锁

认证和授权的区别是什么

  • 认证:根据用户密码或其他信息确认用户是否登录
  • 授权:根据用户提供的令牌或权限判断用户是否可以使用某服务
  • cookie:存在客户端浏览器的数据
  • session:存在服务端的数据

什么是 token?什么是 jwt?

  • token:一种用户登录的加密令牌
  • jwt:生成和解密 token 的算法

spring 的核心是什么

spring 是一个 IOC 和 AOP 容器开源框架

  • IOC:控制反转,由容器帮我们控制对象
  • AOP:面向切面,把代码切入到目标功能
  • 容器:用一系列 map 结构来存储数据

Spring 事物传播机制是什么?

假设容器里由 A 类和 B 类,A 类事物要调用 B 类的话,会设计事物传播

  • REQUIRED:默认传播机制.A、B 类都有事物,可以互相调用.其中一个出现异常则全部回滚
  • NESTED:A 嵌套 B 的事物调用.B 异常只有 B 回滚

还有很多事物传播机制,这里只讲最常用的 2 个

Spring、SpringMVC、Springboot 的区别

  • Spring 是一个 IOC 和 AOP 的开源框架
  • SpringMVC 是一个基于 Spring 的网站开发框架
  • Springboot 是 Spring 生态圈的整合和简化框架

Spring 自动装配原理是什么

自动装配简单来说就是自动将第三方组件的 bean 装在到 ioc 容器里,不需要开发人员再去写 bean 相关配置

在 springboot 应用里面,只需要加上@SpringBootApplication 就可以实现自动装配

@SpringBootApplication 它是一个复合注解,真正实现自动装配的注解是它里面的@EnableAutoConfigruation 这个注解

自动装配主要依靠三个核心技术

  • 找:由于此配置类是放在第三方 jar 包,通过 springboot 中约定大于配置的理念,去把这个配置类的全路径放在 classpath 里的一个 srping.factories 这个文件里面,这样 springboot 就可以通过 springfactoriesLoder 找到第三方组件的配置类在哪
  • 装:引入第三方依赖的 stater ,启动组件的时候这个组件里必须含有@Configuration 配置类.通过其配置类的@Bean 注解找到需要装入 ioc 容器

  • 用:通过 spring 提供的 ImportSelector 这样的接口动态加载对应的 bean

spring 设计模式有哪些

  • 工厂模式
  • 策略模式
  • 代理模式
  • 观察者模式
  • 适配器模式
  • 单例模式
  • 模板模式
  • 装饰者模式

spring 怎么实现事物?

  • 编程式事物:自己通过代码实现
  • 声明式事物:使用@Transcation 实现

事物实现的原理是执行的这个类有@Transcation 时,会把这个类作为 bean 放入 aop 容器中,如果有事物则关闭自动代理,当类的方法执行结束后无异常则把代理对象的事物提交,反之回滚

spring 事物什么时候失效

  • bean 没有被 spring 容器管理
  • 有异常
  • 数据库不支持事物
  • 修饰方法不是 public

讲讲 SpringMVC 的工作流程

这个要有图,心中画好一张图

  • 请求在进入 mvc 前,先值需 HttpMessageConvert,将数据转换,一般是转换为 Json,可以自定义转换数据格式
  • 请求进入 mvc,先到 DispatcherServlet 前端控制器,这个 Servlet 是 javaweb 的一个前端控制器,控制请求是否拦截
  • 请求数据从 DispatcherServlet 到 HandlerMapping,根据 url 找对应的 Controller 类在哪,找到后请求数据返回到 DispatcherServlet 前端控制器
  • 请求数据从 DispatcherServlet 到 HandlerAdapter,根据 url 用来找 Controller 对应的方法
  • 请求从 HandlerAdapter 到 Handler,执行对应 Controller 里的方法,返回 ModleView 或响应数据到 DispatcherServlet 前端控制器
  • 响应从 DispatcherServlet 到 ViewResolver,主要是将响应的数据如果是 ModelView 则找对应的 http 文件渲染,若是 Model 对象则转为 json 对象,最后返回 DispatcherServlet
  • 响应从 DispatcherServlet 到 HttpMessageConvert,将响应数据格式修改,并返回给用户

其中,若有配置拦截器则

  • 请求从 DispatcherServlet 到 HandlerMapping 会触发拦截器的 preHandler
  • 返回 ModleView 或响应数据到 DispatcherServlet 前端控制器会触发拦截器的 postHandler
  • 响应从 DispatcherServlet 到 HttpMessageConvert 会触发拦截器的 afterHandler

springmvc 多个拦截器,它们的执行顺序是如何

  • 先执行拦截器 A 的 preHandler,再执行拦截器 B 的 preHandler
  • 先执行拦截器 B 的 postHandler,再执行拦截器 A 的 postHandler
  • 先执行拦截器 B 的 afterHandler,再执行拦截器 A 的 postHandler

简述 IOC 的工作流程

  • 准备上下文环境:获取扫描 xml 和注解的对象,如 ApplicationContext 的实现类,XmlWebApplicationContex 和 AnnotationConfigApplicationContext,这两个一个是扫描 xml,一个是扫描注解的,而 ApplicationContext 主要是由 BeanFactory 接口实现的
  • 扫描获取 bean 定义,通过扫描获取 BeanDefination,其包含 Bean 的名称、依赖关系等定义信息
  • 实例化 bean 对象:由 ApplicationContext 创建 DefaultListableBeanFactory,由它加载 BeanDefination 从而创建一个单列的 Bean 对象
  • 存储 bean 对象,将 Bean 放到 HashMap 容器里面,使用三级缓存进行缓存

简述 spring bean 的生命周期

springbean 的声明周期分为 5 个阶段

  • 创建前准备:bean 在加载之前从上下文和配置中去解析和查找 bean 有关的拓展实现,比如 init-method(容器初始化 bean 之前执行函数)、destory-method(容器销毁 bean 后执行的函数)、BeanFactoryPostProcessor(bean 前和后实现的函数),这是 spring 提供给开发者使用 bean 之前可自定义拓展的实现,很多第三方组件也经常用到,比如 dubbo
  • 实例化 bean: 通过反射,去创建 bean 的对象
  • 依赖注入:若 bean 含有@Autowrite 注入其他 bean,会去扫描加载对应的 bean
  • 容器缓存:把创建好的 bean 存入容器和缓存
  • 容器销毁:应用上下文关闭时,会销毁 bean,并执行 destory-method 函数

spring 的 bean 是如何获取的/什么是三级缓存/spring 如何解决循环依赖

循环依赖问题是 AB 两个 Bean 相互依赖产生的,一般在设计中我们不允许这种情况出现,而 Srping 提供了三级缓存的方案来解决循环依赖问题

  • 字段注入/set 注入,遇到循环依赖不会报错,但是使用循环依赖的 bean 会报错

1.获取 A 对象,在 1-3 级缓存中查找有无 A 对象

2.无 A 对象,通过反射实例化对象,并将 A 对象的对象工厂存入 3 级缓存,并将 A 的 id 保存

3.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象

4.无 B 对象,通过反射实例化对象,并将 B 对象的对象工厂存入 3 级缓存,并将 B 的 id 保存

5.反射尝试初始化 B 对象,发现 B 对象依赖 A 对象,在 1-3 级缓存中查找有无 A 对象

6.有 A 对象的对象工厂,通过对象工厂获取实例化的对象,并将 A 的实例化对象放入 2 级缓存,删除 3 级缓存的 A 对象工厂

7.B 对象获取到 A 的实例化对象,B 对象初始化完成,将 B 的初始化对象放入 1 级缓存,删除 3 级缓存的 B 对象工厂

8.A 对象获取 B 对象的初始化对象,A 对象初始化完成,将 A 的初始化对象放入 1 级缓存,删除 2 级缓存的 A 对象工厂

9.由于 A 对象引用从实例化变为初始化,所以 B 对象也拿到了 A 对象的初始化对象

  • 构造器注入,遇到循环依赖会直接报错

1.获取 A 对象,在 1-3 级缓存中查找有无 A 对象

2.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象,无 A 对象将 A 的 id 保存 3.反射尝试初始化 B 对象,发现 B 对象依赖 A 对象,在 1-3 级缓存中查找有无 A 对象,无 B 对象并将 B 的 id 保存 4.反射尝试初始化 A 对象,发现 A 对象依赖 B 对象,在 1-3 级缓存中查找有无 B 对象,无 A 对象将 A 的 id 保存,发现已保存,则说明有循环依赖,直接报错

如何理解 springboot 的 stater 依赖

传统的 spring 如果想要加载第三方组件,需要写冗余的 xml 文件,springboot 不需要写,因为它的 stater 依赖里面含有对应的配置类

BIO、NIO、AIO 分别是什么

  • BIO:同步堵塞 IO
  • NIO:异步堵塞 IO
  • AIO:NIO 的 2.0,也是一种异步堵塞 IO,区别是 AIO 有数据需要读的时候再通知给线程

网络通信中一般如何管理线程

一般使用 selector 管理线程,即一个线程有多个 selector,1 个 selector 管理多个 channel

而在 netty 中,eventLoop 就相对于 selector,一个 eventLoopGroup 管理多个 eventLoop,一个 eventLoop 管理多个 channel

当 channel 有读写任务,selector 就会分配线程给他,没有则不分配.

三握手四挥手

三次握手是建立连接时使用

  • 第一次握手:客户端发送 syn 给服务端,服务端把连接信息存入半连接队列,询问是否能连接
  • 第二次握手:服务端确认可以连接,给客户端发送 syn 和 ack
  • 第三次握手:客户端收到确认后,给服务端发送 ack,服务端把连接信息从半连接队列移到全连接队列,连接建立(这样可以防止一下子很多连接,全连接内存爆了)

四次挥手是关闭连接时使用

  • 第一次挥手:客户端发送 fin 给服务端,表示要关闭连接
  • 第二次挥手:服务端发送 ack 给客户端,表示自己准备关闭
  • 第三次挥手:服务端可能还有数据没发完,彻底发完后,发送 fin 给客户端表示可以关闭连接
  • 第四次挥手:客户端收到后发送 ack 给服务端,双方关闭连接

什么是 MQTT 协议

MQTT 协议是一种发布订阅协议,主要由发送者、代理者、订阅者构成

A 客户端想要给 B 客户端发送消息,可以先发送给服务端(发布),服务端再把信息发送个客户端 B(订阅)

它的格式由:

  • 2 字节固定报头:第一个字节消息类型、DUP、可靠等级、RETAIN,第二个字节包含剩余长度信息
  • 可变报头:根据固定报头消息类型,可变包头携带信息不同
  • 消息

try 和 finaly 都有 return,finaly 的 return 还会执行吗

会,finally 中有 return 会覆盖其他块的 return

同包下的类怎么调用

同包下的类可以直接调用,不使用 import

if(1){}可以执行吗

不能,c++中可以,但 java 中不行

什么是索引

索引是一种数据结构,通过在每行数据中添加索引来

说一下数据库查询优化方法

  • 使用 limit
  • 使用索引
  • 开慢查询日志
  • 规划化数据库设计:使用 NF3 模式
  • 页合并:B+树每个节点只有 16kb,也就是 6048 字节存储空间,能存储的数据也就只有 6048/(数据类型+6)的内容(6 是索引的字节),若数据满了就会往下开一个节点,导致查询变慢,页合并就是匀一定数据给其他节点多余空间的节点
  • 页分裂:将一个节点变成 2 个节点,而不是往下开节点,当然,开的节点不能超过 m 阶

进程、线程、协程的区别

  • 进程:计算机的一个基本单元,一个应用
  • 线程:进程的一个执行单元
  • 协程:线程的一个执行单元,一种轻便线程,具有挂载方法和切换线程的功能

Rxjava/协程+线程池+Mapeduce 结合思想

这是我自己发明的

进程调度算法有哪些

  • 先来先去
  • 时间片轮
  • 最短进程有效
  • 最短剩余时间优先
  • 最高响应优先
  • 最高响应比优先
  • 多级反馈发:设置优先级,按顺序进队列,有空余的补上,有优先级高的补上

什么是字节序的大端和小段

  • 大端:高位字节在低地址

  • 小段:低位字节在低地址

判断大端小端用 c++的联合与指针

linux 中常用命令

  • cat/tail 查看日志
  • ps -ef|grep 进程名 根据进程名获取 PID
  • netstat -tuln|grep 端口号 根据端口号获取 PID
  • kill PID 杀死进程

docker/docker-compose 的常用命令

  • docker logs/run/start 查看日志/创建并启动/启动

  • docker-compose up -d --build 安装并后台启动

  • docker exect 进入容器内部

  • docker pull/tag/push 拉取/标记/上传,一般结合 dockerHub 用

数据库的存储过程和触发器是什么

  • 存储过程:可以将特定的 sql 语法存储起来,它由类型、约束条件和 create procedue 构成
  • 触发器:一种特殊的存储过程,执行特定的语法如 insert、update 等可以触发我们的逻辑,它会自动触发

redis支持事务吗?

redis通过mutil来控制事务,但是不同于传统事务ACID,不支持回滚

redis数据结构有哪些?

  • String:一般用于复杂的计数功能的缓存:微博数,粉丝数等,底层实现方式

  • hash:Hash适合用于存储对象,当数据较少时使用ziplist,数据多时用dict

  • list:比如微博的关注列表,粉丝列表,消息列表,使用双向链表Linkedlist实现,redis3后改为quiklist

  • set: set是一个存放不重复值的无序集合,如果数据是整数且少于512个,用intset,否则用dict来存集合
  • zset: zset 相比 set 多了一个权重参数score,即有序的set,当数据少于128个用ziplist,否则使用skiplist+dict

redis数据底层结构分别是什么

  • sds:动态字符串sds 或者 long,存的是字符串用sds,存的是数字用Long .sds是一种动态字符串,由已占用空间的长度、剩余可用空间的长度、数据空间构成
  • dict:一种key-value映射关系的数据结构
  • ziplist:一个经过特殊`编码的双向链表,普通双向链表,存的数据再内存中不是连续的,会有大量内存碎片,而ziplist将双向链表的数据进行特殊编码使它们连续
  • quiklist:一种特殊的ziplist,ziplist由于数据连续,所以查询效率高,但是更新效率就很慢,因为每次更新都要重新编码,而quicklist的每个节点都是一个ziplist,优化了此问题
  • intset:一个由整数组成的有序集合
  • skiplist:一种特殊收到set集合,每两个相邻节点之间有一个索引,从而构成一颗树,通过二分法课快熟查到数据

redis的持久化有哪些,都是怎么实现的

  • RDB:默认方式,隔一段时间保存一次数据
  • AOF:保存操作的记录

redis 是如何删除过期的键

  • 主动:当用户访问对应的 key 时,若超时则删除
  • 被动:redis 每隔 10s 随机抽 20 个 key,有超时则删除,若有 25%以上的 key 超时则再随机抽,重复步骤

redis 集群有哪些

  • 主从复制:一个主 Master,多个从 Slave
  • 哨兵模式 sentinel:在主从复制模式上加了个 sentinel 监控 Master 状态,宕机则重选
  • redis cluster:不同于主从复制,其没有主节点,各个节点之间可互相访问,但可能会出现宕机数据缺失,最好的模式是各个节点配置一个子节点只关联该节点

什么是 redis 脑裂问题,如何解决

脑裂问题主要是 sentinel 哨兵模式下出现的问题,由于网络问题,主节点无法访问子节点,sentinel 重选主节点,网络恢复后两个主节点数据不一致

解决方法:限制主节点数量

什么是缓存雪崩、穿透、击穿,如何解决

  • 雪崩:key 同时失效,大量数据访问数据库,导致数据库宕机.解决方法:使用多级缓存+使用锁
  • 穿透:空数据没有缓存,大量空数据访问数据库,导致数据库宕机解决方法:空数据缓存 null 就行
  • 击穿:热点 key 失效时,大量请求访问数据库,导致数据库宕机解决方法:热点 key 过期时间长一点或者不让其过期

mybatis 的#和$有什么区别

它们都是 sql 参数的替换符,但是#可以防止注入

mysql 引擎有哪些?/聚簇和非聚簇有什么区别

索引和数据一起存放的引擎叫聚簇

  • innoDb:索引和数据一起存放,使用 b+树
  • myisam:只存非聚簇索引,使用哈希数

mysql 中的索引有什么用?索引结构有哪些?

索引就类似查字典时可以通过查询偏旁来查找字,它可以提供我们查询效率

  • B+树:数据满了则开一个子叶存数据
  • 哈希树:数据无序,查询单个数据快,范围查询慢,不能排序

Redis 为什么是单线程

  • redis 基于内存的,cpu 不影响其效率
  • 单线程没有数据同步问题,不需要锁

MQ

什么是 MQ

mq 是一种先进先出的消息队列,是一种常用的异步通信框架,它可以实现消息的异步处理、流量消峰、日志处理等

RabbitMQ 和 kafka 的区别

RabbimtMQ 和 Kafka 的区别总的可以概括为 7 种:

  • 结构区别:RabbitMQ 是一种消息代理模式,主要结构是生产者、交换机、队列、消息、消费者.而 Kafka 是一种分布式流模式,主要结构是生产者、分区、消费者构成.
  • 消息顺序:RabbitMQ 消息顺序发送时可能会出现消费失败消息返回队列而导致原本有序变乱序.而 kafka 的 1 消费者就是一个分区,不会出现收到其他分区消息,也就不会出现乱序
  • 消息路由:RabbitMQ 可以使用交换机对消息进行过滤分发(广播、路由、话题模式),而 kafka 没有这种功能
  • 消息时序:RabbitMQ 可以指定消息的存活时间和消息多久进入队列,而 kafka 只支持指定分区内所有消息的存活时间
  • 消息留存:RabbitMQ 消息被消费了就直接删除,而 kafka 是没有到达分区的存活时间则不会删除
  • 容错处理:RabbitMQ 提供了消息重试和死信交换机的机制,而 Kafka 没有这种功能,需要手动实现
  • 处理速度:RabbimtMQ 在 IO 处理上慢于 Kafka

因此,如果有大数据处理业务,如日志监听、数据 ETL 等,可以使用 Kafka.如果有消息发送、定时任务等需求可以使用 RabbitMQ

RabbitMQ 的工作模式有哪些

MQ 共有 5 种工作模式

  • simple 模式:1 个生产者、1 个队列、1 个消费者
  • work 模式:1 个生产者、1 个队列、多个消费者
  • 广播模式:1 个生产者、1 个交换机、多个队列、多个消费者.交换机可将消息分发到不同的队列
  • 路由模式:广播模式的升级版,交换机分发消息时会根据携带的 key 进行分发
  • 话题模式:路由模式的升级版,交换机分发消息时携带的 key 支持一定规则

RabbitMQ 常见的问题有哪些

  • 消息顺序问题:生产者发送有序的消息给 1 个队列,多个消费者获取消息,但是消费者处理消息时间不同,可能导致消费者消费完成的顺序不是有序的
  • 消息丢失问题:生产者发送消息,交换机收发,队列收发,消费者受消息可能出现网络波动、系统中断等问题导致消息丢失.生产者丢失,开启消息事物或 confirm 确认模式,Mq 队列丢失开启持久化,消费者丢失关闭自动 ack
  • 消息重复问题:消费者消费完数据准备提交确认但网络波动导致提交失败会导致队列重复发送消息.可使用 redis
  • 消息堆积问题:

Nacos 如何热更新配置的

  • 配置变更传播机制:在 nacos 更新配置后,nacos 会通过长轮询机制心跳检测、本地缓存和配置本地监听器的方式更新服务的本地配置
  • Spring 层的配置刷新机制:Nacos 更新完成后触发@RefreshScope,@RefreshScope 会更新对应的 Bean 的@Value
CC BY-NC-SA 4.0 Deed | 署名-非商业性使用-相同方式共享
最后更新时间:2025-10-22 16:07:22