11. java创建对象有哪些方式?
1. 使用new关键字
2. 反序列化对象数据
3. 反射机制:Class类的forName方法的newInstance()
4. 反射机制: 对应类的getConstructor()的newInstance()
5. 通过clone()函数复制,必须实现Cloneable接口
4. 获取 Class 对象的四种方式
1. 直接该类名称的.class
2. 通过Class.forName传入类的全路径获取
3. 通过对象实例getClass函数获取
4. 通过类加载器ClassLoader.loadClass()传入类的路径获取
3. 动态代理
就是给已经开发好的实体类增加功能的手段,不改变原有的处理逻辑
jdk动态代理类使用步骤
1.定义一个接口及其实现类;
2.自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
3.通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
GCLIB动态代理类使用步骤
1.定义一个类
2.自定义MethodIntercepter并重写intercept方法, 用于拦截增强被代理类的方法,和JDK动态代理的invoke方法类似
3. 通过Enhancer类的create创建代理类
写一个线程安全的懒加载单例
import java.util.Scanner;
class Main{
private volatile static Main instance;
public Main() {
}
public static Main getInstance() {
if(instance == null) {
synchronized (Main.class) {
if(instance == null) {
instance = new Main();
}
}
}
return instance;
}
}
2. I/O 流为什么要分为字节流和字符流呢?
防止转换成字节流进行信息交互,出现乱码问题
为什么ArrayList不是线程安全的,具体来说是哪里不安全?
不是
arraylist大体分三部分:
- 需不需要扩容
- size位置设置值
- 将当前集合的大小加1
部分值为null
索引越界异常
size与add数量不一致
2. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
线程不安全,都是实现set接口
Hashset是哈希表
LinkedHashset是哈希表+链表,保证了插入和取出顺序满足FIFO
TreeSet是红黑树
1. HashMap 和 Hashtable 的区别
线程安全:HashMap不安全,HashTable安全
效率:HashMap效率会高
对null key和nullvalue 支持:HashMap支持,HashTable不支持
初始容量和每次扩容容量大小不同:① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
底层结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。
哈希函数的实现:HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的 hashCode() 值。
HashMap 的底层实现
HashMap 的长度为什么是 2 的幂次方
位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,hash % length 等价于 hash & (length - 1)。
可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。
位运算效率更高,数据分布更均匀,扩容机制变得简单
列举HashMap在多线程下可能会出现的问题?
尾插法:会出现数据覆盖
头插法:会在扩容机制中出现死循环,扩容后需要重新分配位置
6. 列举HashMap在多线程下可能会出现的问题?
7. ConcurrentHashMap 和 Hashtable 的区别
底层数据结构:分段数组和链表。jdk8之后是数组和链表/红黑树;hashtable是数组和链表
实现线程安全的方式:ConcurrentHashMap直接对node加锁 cas; hashtable是整个一把锁,效率低下,使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
ConcurrentHashMap已经用了synchronized,为什么还要用CAS(乐观锁)呢
为什么HashMap要用红黑树而不是平衡二叉树?
平衡二叉树追求的是一种 “完全平衡” 状态:任何结点的左右子树的高度差不会超过 1,优势是树的结点是很平均分配的。这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
红黑树不追求这种完全平衡状态,而是追求一种 “弱平衡” 状态:整个树最长路径不会超过最短路径的 2 倍。优势是虽然牺牲了一部分查找的性能效率,但是能够换取一部分维持树平衡状态的成本。与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。
HashMap的扩容机制介绍一下
hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤:
第1步是对哈希表长度的扩展(2倍)
第2步是将旧哈希表中的数据放到新的哈希表中。
只需要看原来的hash值新增的那个bit是0还是1,0是不动,1是移动原始容量距离
请简要描述线程与进程的关系,区别及优缺点?
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
有了进程为什么还需要线程?
- 进程切换是一个开销很大的操作,线程切换的成本较低。
- 线程更轻量,一个进程可以创建多个线程。
- 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
- 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核
4. 如何创建线程?
继承Thread,实现Runnable接口、实现Callable接口,实现线程池
最终都是 new Thread.start()
说说线程的生命周期和状态?
初始
运行
阻塞
等待
超时等待
终止
7. Thread#sleep() 方法和 Object#wait() 方法对比
共同点:两者都可以暂停线程的执行。
区别:
sleep()方法没有释放锁,而wait()方法释放了锁 。wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)超时后线程会自动苏醒。sleep()是Thread类的静态本地方法,wait()则是Object类的本地方法。
3. 并发编程三个重要特性
原子性:
可见性:修改是可见的
有序性:volatile关键字可以禁止指令进行重排序优化。
4. volatile 关键字
4. 如何实现乐观锁?
版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功
CAS:CAS 涉及到三个操作数:
- V:要更新的变量值(Var)
- E:预期值(Expected)
- N:拟写入的新值(New)
4. synchronized 和 volatile 有什么区别?
互补
volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
synchronized 和 ReentrantLock 有什么区别?
都是可重入锁
synchronized是依赖于JVM,ReentrantLoack依赖于API
synchronized是非公平的,ReentrantLoack可以是公平或者非公平
ReentrantLoack可实现等待可中断
可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
支持超时 :ReentrantLock 提供了 tryLock(timeout) 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待
ThreadLocal 内存泄露问题是怎么导致的?如何避免?
ThreadLocal实例不再被强引用;key为null- 线程持续存活,导致
ThreadLocalMap长期存在。
如何避免:
在使用完
ThreadLocal后,务必调用remove()方法。 这是最安全和最推荐的做法。remove()方法会从ThreadLocalMap中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将ThreadLocal定义为static final,也强烈建议在每次使用后调用remove()。在线程池等线程复用的场景下,使用
try-finally块可以确保即使发生异常,remove()方法也一定会被执行。
5. 如何跨线程传递 ThreadLocal 的值?
InheritableThreadLocal:InheritableThreadLocal是 JDK1.2 提供的工具,继承自ThreadLocal。使用InheritableThreadLocal时,会在创建子线程时,令子线程继承父线程中的ThreadLocal值,但是无法支持线程池场景下的ThreadLocal值传递。TransmittableThreadLocal:TransmittableThreadLocal(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持ThreadLocal值传递
3. 如何创建线程池?常用方法
start启动线程,线程池:submit对于callable,execute对于runable。
7. 线程池的拒绝策略有哪些?
12. 线程池中线程异常后,销毁还是复用?
3. 一个任务需要依赖另外两个任务执行完之后再执行,怎么设计?
1. 对象的创建过程
内存分配和回收原则
死亡对象判断方法
4. 如何判断一个常量是废弃常量?
如何判断一个类是无用的类?
垃圾收集算法
垃圾收集器
类加载过程
JVM 中内置了三个重要的 ClassLoader
类加载器和双亲委派机制。
1. 说说自己对于 Spring MVC 了解?
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
3. 将一个类声明为 Bean 的注解有哪些?
@Component:通用的注解,可标注任意类为Spring组件。如果一个 Bean 不知道属于哪个层,可以使用@Component注解标注。@Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。@Service: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller: 对应 Spring MVC 控制层,主要用于接受用户请求并调用Service层返回数据给前端页面。
11. Bean 的生命周期了解么?
创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如
@Autowired等注解注入的对象、@Value注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。Bean 初始化
- 如果 Bean 实现了
BeanNameAware接口,调用setBeanName()方法,传入 Bean 的名字。 - 如果 Bean 实现了
BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 - 如果 Bean 实现了
BeanFactoryAware接口,调用setBeanFactory()方法,传入BeanFactory对象的实例。 - 与上面的类似,如果实现了其他
*.Aware接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 - 如果 Bean 实现了
InitializingBean接口,执行afterPropertiesSet()方法。 - 如果 Bean 在配置文件中的定义包含
init-method属性,执行指定的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor对象,执行postProcessAfterInitialization()方法。
- 如果 Bean 实现了
销毁 Bean
:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
- 如果 Bean 实现了
DisposableBean接口,执行destroy()方法。 - 如果 Bean 在配置文件中的定义包含
destroy-method属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy注解标记 Bean 销毁之前执行的方法。
- 如果 Bean 实现了
整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
初始化这一步涉及到的步骤比较多,包含
Aware接口的依赖注入、BeanPostProcessor在初始化前后的处理以及InitializingBean和init-method的初始化操作。销毁这一步会注册相关销毁回调接口,最后通过
DisposableBean和destory-method进行销毁。

3. SpringMVC 工作原理了解吗?
- 客户端(浏览器)发送请求,
DispatcherServlet拦截请求。 DispatcherServlet根据请求信息调用HandlerMapping。HandlerMapping根据 URL 去匹配查找能处理的Handler(也就是我们平常说的Controller控制器) ,并会将请求涉及到的拦截器和Handler一起封装。DispatcherServlet调用HandlerAdapter适配器执行Handler。Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。Model是返回的数据对象,View是个逻辑上的View。ViewResolver会根据逻辑View查找实际的View。DispaterServlet把返回的Model传给View(视图渲染)。- 把
View返回给请求者(浏览器)

上述流程是传统开发模式(JSP,Thymeleaf 等)的工作原理。
对于前后端分离时,后端通常不再返回具体的视图,而是返回纯数据(通常是 JSON 格式,Spring 会自动将其转换为 JSON 格式),由前端负责渲染和展示。
1. Spring 循环依赖了解吗,怎么解决?
两个或多个 Bean 之间相互持有对方的引用。
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
}
@Component
public class CircularDependencyB {
@Autowired
private CircularDependencyA circA;
}
有三级缓存:
- 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充,未进行依赖注入),也就是三级缓存中
ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。 - 三级缓存(singletonFactories):存放
ObjectFactory,ObjectFactory的getObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
- 先去 一级缓存
singletonObjects中获取,存在就返回; - 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects中获取; - 如果还没有获取到,就去 三级缓存
singletonFactories中获取,通过执行ObjectFacotry的getObject()就可以获取该对象,获取成功之后,从三级缓存移除B,并将B对象加入到二级缓存中。
步骤 1:创建 Bean A
- Spring 容器开始创建 Bean A,首先将 Bean A 的创建状态标记为 “正在创建”。
- 实例化 Bean A,将 Bean A 的早期引用(一个
ObjectFactory对象)放入三级缓存singletonFactories中。这个ObjectFactory对象可以在需要时返回 Bean A 的早期实例。 - 开始对 Bean A 进行属性注入,发现 Bean A 依赖于 Bean B。
步骤 2:创建 Bean B
- Spring 容器开始创建 Bean B,同样将 Bean B 的创建状态标记为 “正在创建”。
- 实例化 Bean B,将 Bean B 的早期引用放入三级缓存
singletonFactories中。 - 开始对 Bean B 进行属性注入,发现 Bean B 依赖于 Bean A。
步骤 3:解决 Bean B 对 Bean A 的依赖
- 当 Bean B 需要注入 Bean A 时,Spring 容器首先从一级缓存
singletonObjects中查找 Bean A,发现没有找到。 - 接着从二级缓存
earlySingletonObjects中查找,也没有找到。 - 然后从三级缓存
singletonFactories中查找,找到了 Bean A 的早期引用(ObjectFactory对象)。 - 调用
ObjectFactory的getObject()方法,获取 Bean A 的早期实例。如果 Bean A 需要进行 AOP 代理,会在这里创建代理对象,并将代理对象放入二级缓存earlySingletonObjects中,同时从三级缓存singletonFactories中移除该引用。 - 将获取到的 Bean A 的早期实例注入到 Bean B 中。
步骤 4:完成 Bean B 的创建
- Bean B 完成属性注入后,进行初始化操作。
- 将完全创建好的 Bean B 实例放入一级缓存
singletonObjects中,并从二级缓存earlySingletonObjects和三级缓存singletonFactories中移除相关引用。
步骤 5:完成 Bean A 的创建
- 由于 Bean B 已经创建完成,将 Bean B 实例注入到 Bean A 中。
- Bean A 完成属性注入后,进行初始化操作。
- 将完全创建好的 Bean A 实例放入一级缓存
singletonObjects中,并从二级缓存earlySingletonObjects和三级缓存singletonFactories中移除相关引用。
1. 什么是 Spring Boot Starters?
Spring Boot Starters 是一组便捷的依赖描述符,它们预先打包了常用的库和配置。当我们开发 Spring 应用时,只需添加一个 Starter 依赖项,即可自动引入所有必要的库和配置,快速引入相关功能。
在没有 Spring Boot Starters 之前,开发一个 RESTful 服务或 Web 应用程序通常需要手动添加多个依赖,比如 Spring MVC、Tomcat、Jackson 等。这不仅繁琐,还容易导致版本不兼容的问题。而有了 Spring Boot Starters,我们只需添加一个依赖,如 spring-boot-starter-web,即可包含所有开发 REST 服务所需的库和依赖。
这个 spring-boot-starter-web 依赖包含了 Spring MVC(用于处理 Web 请求)、Tomcat(默认嵌入式服务器)、Jackson(用于 JSON 处理)等依赖项。这种方式极大地简化了开发过程,让我们可以更加专注于业务逻辑的实现。
2. 介绍一下@SpringBootApplication 注解
- @EnableAutoConfiguration: 启用 Spring Boot 的自动配置机制。它是自动配置的核心,允许 Spring Boot 根据项目的依赖和配置自动配置 Spring 应用的各个部分。
- @ComponentScan: 启用组件扫描,扫描被 @Component(以及 @Service、@Controller 等)注解的类,并将这些类注册为 Spring 容器中的 Bean。默认情况下,它会扫描该类所在包及其子包下的所有类。
- @Configuration: 允许在上下文中注册额外的 Bean 或导入其他配置类。它相当于一个具有 @Bean 方法的 Spring 配置类。
3. Spring Boot 的自动配置是如何实现的?
Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类实现自动装配.
HTTP和HTTPS二者区别
- HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
- HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
- 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
- HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
对称+非对称、摘要算法+数字签名、身份证书
TCP、UDP区别、应用场景
MySQL 基础架构
1. 结构
- 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
- 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
- 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
- 优化器: 按照 MySQL 认为最优的方案去执行。
- 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
- 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。InnoDB 是 MySQL 的默认存储引擎,绝大部分场景使用 InnoDB 就是最好的选择。

2. SQL语句在MySQL中的执行过程
- 查询语句的执行流程如下:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎
- 更新语句执行流程如下:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit 状态)
Redis单线程怎么监视IO连接?
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
Redis I/O复用的三种实现方式?复杂度
建议deepseek问一下。
select:轮寻,O(n)
poll:轮寻,O(n)
Epoll: 基于事件触发,O(1)
Redis ZSet的底层实现
重点关注跳表的原理,复杂度,为什么要采用两种实现。
缓存击穿、缓存穿透、缓存雪崩
如何解决?是结合黑马点评问的。
如何保障数据库和缓存的一致性。
旁路缓存模式:先修改数据库,在删除缓存。
说说hashmap,线程安全吗?哈希冲突怎么解决?说说扩容?升级红黑树的阈值,降为链表的阈值?为什么二者不一样。
看javaguide。
阈值是8和6,防止频繁的转换。
聊聊JVM的内存区域
很重要,建议javaguide。
线程的:栈、程序计数器。
进程的:堆。方法区
聊聊垃圾回收器G1的回收过程?优点是什么?
javaguide。
聊聊对象在JVM的移动(Eden->S1和S2->老年代),为什么老年区占空间大于新生区?
很重要,注意数组等大对象直接放在老年区。
面向对象三个特点?多态的实现方式?
见javaguide。
重载和重写的区别?
见javaguide。关注一下重载发生在编译器,重写发生在运行期。