Skip to main content

面试题总结

· 57 min read
Happlay71
一个渴望成为技术大佬的小白

java 集合

Collection

存放单一元素

  • List
    • 有序可重复
    • ArrayListObject 数组,不同步,线程不安全
      • 内部基于动态数组实现,比 Array 使用起来更灵活
      • 可以添加 null 值,但是不建议,会难以维护判空异常
    • Vector:古早的以 Object 数组为基础的
    • LinkedList:双向链表(1.6 之前是循环链表,1.7 取消了循环),不同步,线程不安全
      • 因为是由链表构成的,所以不支持随机访问,不能实现 RandomAccess 接口
  • Set
    • 无序不可重复
    • HashSet:基于 HashMap 实现
      • 检查重复:对象加入 HashSet 时,会计算 hashCode 值来判断对象的加入位置,同时也会与其他加入的对象的 hashCode 的值比较,如果没有相符的,则会认为没有重复;否则,会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同
    • LinkedHashSetHashSet 的子类,通过 LinkedHashLMap 实现
    • TreeMap:红黑树实现
  • Queue
    • 按照特定顺序排序,有序可重复
  • Comparable:出自 java.lang 包,有一个 compareTo(Object obj) 方法
  • Comparator:出自 java.util 包,有一个 compare(Object obj1, Object obj2) 方法

Map

以键值对形式存储

  • HashMap:1.8 之前以数组和链表组成,链表是为了解决哈希冲突存在;之后改成了数组/链表+红黑树(当链表长度超过阈值 8 时,先判断数组是否超过 64,超过,将链表转换为红黑树,没超过,先把数组扩容),线程不安全

    • 因为线程安全问题,效率高于 HashTable

    • 可以存储 Null keyNull value,但是 null 键只有一个,null 值可以有多个

    • 默认初始化大小为 16,每次扩容容量变为原来 2 倍,如果给了初始值,则每次扩容容量为 2 的幂次方大小

    • 在 1.7 时,如果多个进行扩容时会造成死循环

      • 第一个线程:在头节点插入前被调度挂起
      • 第二个线程:因为使用的是头插法,原来的头节点插入新链表后,第二个节点头插法插入同一个数组下标下的链表,线程结束
      • 第一个线程:把新数组下标中存储第二个线程的更新后的那个头节点,此时尾节点的后继指向了头节点
    • 1.8 后,多个线程更新同一个桶的数据会产生数据覆盖的风险

  • LinkedHashMap:继承自HashMap,底层是基于拉链式散列结构即数组和链表或红黑树组成,另外多加了双向链表,可以头插和尾插数据,线程不安全

  • HashTable:数组+链表,链表主要为了解决哈希冲突,线程安全(因为内部方法都是用 synchronized 修饰的)

    • 不允许存储 Null keyNull value
  • TreeMap:红黑树,线程不安全

    • 实现了 NavigableMap 接口:有了对集合内元素搜索的能力
    • 实现了 SortedMap 接口:有了对集合中元素根据键排序的能力,默认是按 Key 的升序排序

遍历方式

Concurrenthashmap 是怎么实现线程安全的(具体读写)

  • 1.7 之前主要依赖于分段锁 - Segment
    • 由多个 Segment 组成(默认为 16),每个 Segment 维护一个 HashEntery 数组(数组+链表,类似与 1.7 之前的 HashMap
    • 每个 Segment 继承 ReetrantLock ,用于实现分段锁,每个锁只锁容器其中一部分数据,多线程访问不会存在锁竞争,提高并发访问率
      • 因为继承了 ReetrantLock,所以 Segment 是一种可重入锁,扮演锁的角色
    • 读操作
      • get(key) 不会加锁,直接通过 volatile 变量保证可见性
    • 写操作
      • putremove 需要锁住单个 Segment,并不会影响其他 Segment
      • 计算 keyhash 值,找到 Segment,然后使用 SegmentReentrantLock.lock() 进行加锁操作
      • 插入或删除数据后释放锁
  • 1.8 后彻底移除了 Segment,改用数组+链表+红黑树的数据结构,并使用 CAS + Synchronized 实现线程安全
    • Node<K, V>[] table 作为主数据存储
    • 当表长度超过一定阈值(8)时,转换为红黑树
    • 锁粒度更细, synchronized 只锁定当前链表或红黑二叉树的首节点,只要不产生 hash 冲突就不会产生并发
    • 读操作
      • get(key) 操作不会加锁:
        • 计算 keyhash 值,找到 bucket (数组槽)
        • 直接读取 table[index] ,如果是链表或红黑树,则遍历查找
        • 由于 Nodevalue 使用 volatile 修饰,所以可以保证可见性,无需加锁
    • 写操作:
      • put(key, value)
        • 计算 keyhash 值,找到数组的 index 位置
        • CAS 方法创建新 Node :如果该 bucket 为空,则用 CAS 插入新节点,避免加锁
        • Synchronized 锁定链表或红黑树
          • 如果 CAS 失败,说明该 bucket 已有数据,则用 synchronized 直接锁定该桶的头节点,进行插入
          • 如果是链表,遍历后插入
          • 如果是红黑树,按照红黑树规则插入

红黑树的优势

红黑树是一种自平衡二叉搜索树( BFS

  • 优势
    1. 保持平衡,最坏情况下时间复杂度 O(logn)
      • 普通的二叉搜索树( BFS )在极端情况下会退化成链表(即高度接近 n ),导致 O(n) 的查询复杂度
      • 红黑树通过旋转和变色操作保持平衡,保证最坏情况下的增删改查操作都维持在 O(logn) ,不会出现严重的退化
    2. 插入、删除效率高
      • 插入、删除操作的调整成本较低:自平衡调整(变色 + 最多 2 次旋转)开销较小
    3. 适用于高并发
      • ConcurrentHashMap 中,当桶中的链表长度超过 8 时,转换成红黑树,提高查询性能
      • 插入、删除的调整操作相对较少,在并发环境下更稳定,不会频繁进行全书重平衡
    4. 空间占用较低
      • 红黑树不需要存储额外的平衡因子
      • 只需要额外存储 1 位颜色信息(红/黑)

jvm 虚拟机

数据库的隔离级别

定义了多个事务并发执行时的数据可见性,主要用于控制脏读、不可重复读、幻读等问题

  • 读未提交:最低的隔离级别,事务可以读取其他未提交事务的数据,并发性高,但数据一致性最差
    • 脏读:事务 A 读取了事务 B 未提交的数据,如果 B 回滚, A 读到的数据就是无效的
    • 不可重复读:事务 A 多次读取同一行数据,期间事务 B 修改并提交,导致 A 读取的值不一致
    • 幻读:事务 A 读取了某个范围的数据,事务 B 插入了新数据,导致 A 重新读取时看到了幻影数据
  • 读已提交:只能读取已经提交的数据,避免了脏读,比读未提交安全,仍支持较高的并发性
    • 不可重复读、幻读
  • 可重复读:事务执行期间,读取的所有数据保持一致,防止不可重复读。
    • 幻读
    • Mysql InnoDB 通过 MVCC (多版本并发控制)避免了幻读
  • 可串行化:最高级别,每个事务必须依次执行,完全避免并发问题
    • 可能导致死锁
    • 需要表级锁行级锁,影响系统吞吐量

数据库索引优化措施

volatile

主要用于保证变量的可见性防止指令重排,但不保证原子性,每次使用都要到主内存中读取

  • 当一个变量被 volatile 修饰后,所有线程都能看见它的最新值:JVM 通过内存屏障禁止 CPU 缓存优化,确保变量的修改立即对所有线程可见

  • volatile禁止 CPU 和编译器对代码进行指令重排,保证代码按预期执行顺序运行:通过插入特定的内存屏障来禁止指令重排,例如:双重校验锁实现对象单例(线程安全)

    1. 为变量 uniqueInstance 分配内存空间

    2. 初始化 uniqueInstance

    3. uniqueInstace 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执行顺序可能变成 1-> 3 -> 2。指令在单线程的情况下不会出现问题,但是多线程环境下会导致一个线程获得还没有初始化的实例

  • 不能保证原子性:因为当多个线程同时读取被 volatile 修饰的变量时,可能同时读取到同一个值,而不是依次读取

    • 可以利用 synchronizedLockAtomicInteger来实现正确操作

synchronized

用于保证代码块或方法在多线程环境下的同步执行,以确保原子性、可见性、有序性

  • 保证原子性:确保多个线程同时访问共享资源时,操作不会被中断
  • 保证可见性:线程对变量的修改对其他线程立即可见(因为同步代码块执行时,会刷新 CPU 缓存
  • 保证有序性防止指令重排,确保代码按预期执行

用法

  • 修饰实例方法:锁当前对象

    • synchronized void method() { // 业务代码 }
  • 修饰静态方法:锁当前类,会作用于类的所有对象实例,进入同步代码前要获取当前 class 的锁

    • synchronized static void method() { // 业务代码 }

      静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥:因为访问静态方法占用的锁是当前类的锁,二访问非静态方法占用的锁是当前实例对象锁

  • 修饰代码块:锁指定对象/类,对括号里指定的对象/类加锁

    • synchronized(Object) 表示进入同步代码库前要获得给指定对象的锁
    • synchronized(类.class)表示进入同步代码前要获得给指定 Class 的锁
  • 构造方法不能用 synchronized 修饰,但是可以在方法内部去修饰代码块

底层原理

  • synchronized 底层是依靠 JVM 进入/退出对象的 Monitor (监视器)实现的
    • 获取对象的 Monitor
      • Monitor 空闲,则线程占用 Monitor ,执行代码
      • Monitor 被占用,线程进入阻塞状态,直到获取 Monitor
    • 释放 Monitor
      • 代码执行完毕,或抛出异常,线程释放 Monitor,其他线程才能进入

ReentrantLock

Java 并发包( java.util.concurrent.locks )提供的一种可重入且独占式的锁,相较于 synchronized ,它提供了更灵活的锁管理

  • 可重入锁:也叫递归锁,同一线程可多次获取同一把锁

    • A 线程中开启锁后调用 B 线程(也开启锁),不会阻塞。因为同一线程能多次 lock(),但要匹配 unlock() 的次数
  • 支持公平锁/非公平锁

    • 公平锁:锁被释放后,先申请的线程先得到锁。性能较差,上下文切换更频繁
    • 非公平锁:锁被释放后,后申请的线程可能会先获取锁,是随机或者按照其他优先级排序。性能更好,但可能导致某些线程饿死
  • 支持可中断锁lockInterruptibly() 允许线程在等待锁时被中断,抛出异常(Thread.interrupt()

  • 支持超时获取锁:提供了 tryLock(time, unit) 的方法,尝试在指定时间内获取锁,避免死锁,超时后返回 false ,可以执行其他逻辑,而不会永远阻塞

  • 支持多个条件变量Condition 接口

    • 在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择的进行线程通知,在调度线程上更加灵活。
    • 在使用 notify()/notifyAll() 方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”

synchronized vs ReentrantLock

对比synchronizedReentrantLock
可重入✅ 支持✅ 支持
公平锁❌ 默认非公平✅ 可公平/非公平
阻塞中断❌ 不支持lockInterruptibly() 支持中断
超时获取❌ 不支持tryLock(time, unit) 可超时
性能🚀 低竞争时优化🚀 更灵活

自动装配(Autowiring)

Spring 依赖注入(DI的一种方式,它可以让 Spring 自动根据类型、名称等,找到合适的 Bean 并注入,而无需手动配置

自动装配方式说明
no(默认)不使用自动装配,需手动 @Bean 或 XML 配置
byName根据 属性名 匹配同名 Bean
byType根据 类型 匹配 Bean
constructor根据 构造方法参数 自动装配
autodetect(过时)先尝试 constructor,如果失败,则 byType

自动配置主要由@EnableAutoConfiguration实现,添加了@EnableAutoConfiguration注解,会导入AutoConfigurationImportSelector类,里面的selectImports方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有含有META-INF/spring.factoriesjar包,将对应key@EnableAutoConfiguration注解全名对应的value类全部装配到IOC容器中。

设计模式应用及好处

解决特定问题的最佳实践,能够提高代码的可维护性、复用性和扩展性

设计模式分为三大类:

类型作用常见模式
创建型模式解决对象创建问题,隐藏对象创建逻辑单例、工厂、抽象工厂、建造者、原型
结构型模式解决类与类之间的关系,提高复用性代理、装饰、适配器、桥接、组合、外观、享元
行为型模式解决对象间通信,提高灵活性观察者、策略、命令、责任链、模板方法、状态、迭代器、备忘录、解释器

创建型模式

  • 单例模式:确保一个类只有一个实例,并提供一个全局访问点
  • 工厂模式:在不暴露创建对象的逻辑的前提下,使用工厂方法来创建对象
  • 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的实例,而不需要指定实际实现类
  • 建造者模式:将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
  • 原型模式:通过克隆来创建对象,避免了通过 new 关键字显示调用构造函数的开销

构造型模式

  • 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法一起工作的类可以一起工作
  • 桥接模式:将抽象部分与它的实现部分分离,以便它们可以独立的变化
  • 组合模式:将对象组合成树形结构以表示部分-整体的层次结构,使得客户端使用单个对象或组合对象具有一致性
  • 装饰器模式:动态地给一个对象添加一些额外的职责,就增加功能而言,装饰器模式比生成子类方式更为灵活
  • 外观模式:为子系统中的一组接口提供一个一致的界面,使得子系统更容易使用
  • 享元模式:运用共享技术来有效地支持大量细粒度对象的复用
  • 代理模式:为其他对象提供一种代理以控制对这个对象的访问

行为型模式

  • 责任链模式:为解除请求的发送者和接收者之间的耦合,而将请求的处理对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止
  • 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作
  • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子
  • 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示
  • 中介者模式:用一个中介对象封装一系列的对象交互,使得这些对象不需要显示地互相引用,从而降低耦合度
  • 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
  • 观察者模式:定义对象间的一种一对多的依赖关系,当前对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
  • 状态模式:允许一个对象在其内部状态发生改变时改变其行为,对象看起来似乎改变了它的类
  • 策略模式:定义一系列的算法,将每个算法封装起来,并使它们之间可以互换
  • 模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤
  • 过滤器设计模式:允许在不改变原始对象的情况下,动态地添加或删除对象的行为

设计模式六大原则

  • 单一职责原则:一个类应该只有一个引起它变化的原因。一个类应该只有一项职责,这样可以保证类的内聚性,并且降低类之间的耦合
  • 开闭原则:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。当需要添加新功能时,应尽量通过扩展已有代码来实现,而不是修改已有代码。
  • 里氏替换原则:子类应该能够替换父类并且不影响程序的正确性。这意味着在使用继承时,子类不能修改父类已有的行为,而只能扩展父类的功能
  • 接口隔离原则:客户端不应该依赖于它不需要的接口。一个类应该只提供它需要的接口,而不应该强迫客户端依赖于它不需要的接口
  • 依赖倒置原则:高层模块不应该依赖于底层模块,都应该依赖于抽象。抽象不应该依赖于具体实现,而具体实现应该依赖于抽象
  • 迪米特法则:一个对象应该对其他对象保持最少的了解。一个对象只应该与它直接作用的对象发生交互,而不应该与其他任何对象发生直接交互。可以降低类之间的耦合性,提高系统的灵活性和可维护性

组合使用

  • 工厂模式 + 单例模式:使用工厂模式创建对象,通过单例模式来保证该工厂只有一个实例,从而减少创建对象时的开销
  • 模板方法模式 + 策略模式:使用模板方法模式来定义算法的骨架,同时使用策略模式来定义算法的不同实现方式,以实现更高的灵活性
  • 策略模式 + 工厂模式:使用工厂模式创建不同的策略对象,然后使用策略模式来选择不同的策略,以实现不同的功能
  • 适配器模式 + 装饰器模式:适配器模式用于一个接口转换成另一个接口,而装饰器模式则用于动态地给对象添加一些额外的职责。
  • 观察者模式 + 命令模式:观察者模式用于观察对象的状态变化,并及时通知观察者。而命令模式则用于将一个请求封装成一个对象,可以在运行时动态地切换命令的接收者。当我们需要观察对象的状态变化,并在状态变化时执行一些命令时,可以使用

好处

提高代码复用性,减少重复代码 ✅ 降低代码耦合度,使模块独立 ✅ 增强扩展性,新增功能时修改更少 ✅ 符合 SOLID 设计原则,提高软件质量

如何处理异常

@RestController 和@Controller 的区别

@RestController

  • 适用于 RESTful API
  • 继承自 @Controller ,并自动包含 @ResponseBody
  • 方法返回值默认是 JSONXML (基于 JacksonGson 进行序列化)
  • 不需要手动加 @ResponseBody

@Controller

  • 适用于返回视图(如 ThymeleafJSP

  • 需要手动使用 @ResponseBody 才能返回 JSON

  • 默认返回的是一个视图( HTML 页面),而不是 JSON

    如果在方法上加上 @RequestBody 则该 API 不会返回视图,而是返回 JSON 数据

java 里的 json

常用的 JSON 解释库包括

  1. JacksonSpring 默认推荐)

    // 对象 转 json
    String json = objectMapper.writeValueAsString(user);
    // json 转 对象
    User userObj = objectMapper.readValue(json, User.class);
    // json 转 map
    Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
  2. GsonGoogle 提供)

    // 对象 转 json
    String json = gson.toJson(new User("Bob", 30));
    // json 转 对象
    User user = gson.fromJson(json, User.class);
  3. FastJSON (阿里巴巴提供,性能较优)

    // 对象 转 json
    String json = JSON.toJSONString(new User("Charlie", 28));
    // json 转 对象
    User user = JSON.parseObject(json, User.class);
  4. org.json (官方 JSON 解析库)

    import org.json.JSONObject;

    JSONObject jsonObject = new JSONObject("{\"name\":\"David\",\"age\":40}");
    System.out.println(jsonObject.getString("name")); // David
JSON 库适用场景优势劣势
JacksonSpring Boot 默认功能强大,支持 POJO 直接转换依赖较多
Gson轻量级 JSON 解析API 简单易用速度稍慢
FastJSON高性能 JSON 解析解析速度快安全性争议
org.json轻量级官方库纯粹 JSON 处理功能较少

正向、反向代理

正向代理(服务端看不见客户端)

客户端通过代理服务器访问目标服务器。客户端先向代理服务器发送请求,代理服务器在代表客户端去访问目标服务器,并将结果返回给客户端。

流程:

  • 客户端请求访问目标服务器,但无法直接访问
  • 请求先发送到正向代理服务器
  • 代理服务器转发请求到目标服务器,获取响应数据
  • 代理服务器将数据返回给客户端

反向代理(客户端看不见服务端)

客户端不能直接访问目标服务器,而是通过代理服务器访问,代理服务器再去请求目标服务器,并将结果返回给客户端

用于:

  • 负载均衡
  • 隐藏真实服务器
  • 安全防护(防 DDoS )
  • 缓存加速

流程:

  • 客户端向代理服务器(反向代理)发送请求
  • 反向代理服务器将请求转发给后端真实服务器
  • 真实服务器返回响应数据给反向代理服务器
  • 反向代理服务器将数据返回给客户端

== 和 equals 的区别

对于字符串变量来说,使用 """equals" 比较字符串时,其比较方法不同。"" 比较两个变量本身的值,即两个对象在内存中的首地址,"equals" 比较字符串包含内容是否相同

对于非字符串变量来说,如果没有对 equals() 进行重写的话,"==""equals" 方法的作用是相同的,都是用来比较队形在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象

  • ==:比较的是两个字符串内存地址的数值是否相等,属于数值比较
  • equals:比较的是两个字符串的内容,属于内容比较

java final 关键字作用

  • 修饰类:当 final 修饰类时,表示这个类不能被继承,是类继承体系中的最终形态。如,String 类就是用 final 修饰,保证了 String 类的不可变性和安全性

  • 修饰方法:用 final 修饰的方法不能在子类中被重写。

  • 修饰变量:当 final 修饰基本数据类型的变量时,该变量一旦被赋值就不能再更改。对于引用数据类型,final 修饰意味着这个引用变量不能再指向其他对象,但是对象本身的内容可以改变。

Spring IOC 的实现原理

IOC:控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;而在 Spring 程序中,IOC 是有专门的容器去控制对象。

所谓控制就是对象的创建、初始化、销毁:

  • 创建对象:原来是 new 一个,现在是 Spring 容器创建

  • 初始化对象:原来是对象自己通过构造器或 setter 方法给依赖的对象赋值,现在由 Spring 容器自动注入

  • 销毁对象:原来是直接给对象复制 null 或做一些销毁操作,现在是 Spring 容器管理生命周期负责销毁对象

所谓反转:是反转控制权,我们由对象的控制者变成了 IOC 的被动控制者

深拷贝和浅拷贝的区别

  • 浅拷贝:只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。

  • 深拷贝:在复制对象的同时,将对象的所有引用类型字段的内容也复制一份,而不是共享引用。

mybatis 中 # 和 $ 符号的区别

  • Mybatis 在处理 #{} 时,会创建预编译的 SQL 语句,将 SQL 中的 #{} 替换为 ? 号,在执行 SQL 时会为预编译 SQL 中的占位符 (?) 赋值,调用 PreparedStatementset 方法来赋值,预编译的 SQL 语句执行效率高,并且可以防止 SQL 注入,提供更高的安全性,适合传递参数

  • Mybatis在处理 ${}时,只是创建普通的SQL语句,然后在执行SQL语句时Mybatis将参数直接拼入到SQL里,不能防止SQL注入,因为参数直接拼接到SQL 语句中,如果参数未经过验证、过滤、可能会导致安全问题

maven 中 install 和 package 区别

  • package:此命令会对项目进行编译、测试,然后把项目打包成特定格式的文件,像 JARWAR

  • install:该命令不但会完成 package 的操作,还会把打包好的文件安装到本地的 Maven 仓库里,方便其他项目引用

maven 怎么管理不同的版本号

  • 直接指定版本号

    <dependencies>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>...</version>
    </dependency>
    </dependencies>
  • 使用属性管理版本号

    <properties>
    <junit.version>4.13.2</junit.version>
    </properties>
    <dependencies>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
    </dependency>
    </dependencies>
  • 依赖范围管理

    <dependencies>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
    </dependency>
    </dependencies>

线程并发的场景里保证线程安全

  • synchronized:可以使用 synchronize 关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问。对象锁是通过 synchronized 关键字锁定对象的监视器来实现

  • volatilevolatile 关键字用于变量,确保所有线程看到的是改变量的最新值,而不是可能存储在本地寄存器的副本

  • Lock 接口和 ReentrantLock 类:java.util.concurrent.locks.Lock 接口提供了比 synchronized 更强的锁锁定机制,ReentrantLock 实现该接口,提供了更灵活的锁管理和更高的性能

  • 原子类:Java 并发库提供了原子类,如 AtomicIntegerAtomicLong 等,这些类提供了原子操作,可以用于更新基本类型而无需额外的同步

  • 线程局部变量:ThreadLocal 类可以为每个线程提供独立的副本,每个线程都有自己的变量

  • 并发集合、 JUC 工具类

SpringAOP

用于实现切面编程

功能:

  • 核心业务:登录、注册、crud

  • 周边功能:日志、事务管理

在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,二者不是耦合的然后把切面功能和核心业务功能组合,叫 AOP

AOP 便于减少系统的重复代码、降低模块间的耦合度,有利于未来的可拓展性和可维护性

  • AspectJ:切面,没有具体的接口或类与之对应,是 Join pointAdvicePointcut 的一个统称

  • Join point:连接点,指程序执行过程中的一个点,例如方法调用、异常处理等。

  • Advice:通知,即定义一个切面中的横切逻辑,有 aroundbeforeafter 三种类型。在很多的 AOP 实现框架中,Advice 通常作为一个拦截器,也可以包含多个拦截器作为一条链路围绕着连接点进行处理

  • Pointcut:切点,用于匹配连接点,一个切面中包含哪些 Join point 需要由 Pointcut 进行筛选

  • Introduction:引介,让一个切面可以声明被通知的对象实现任何他们没有真正实现的额外的接口。

  • Weaving:织入,在有了连接点、切点、通知以及切面,通过织入,在切点的引导下,将通知逻辑插入到目标方法上,使得我们的通知逻辑在方法调用时得以执行

  • AOP proxyAOP 代理,指在 AOP 实现框架中实现切面协议的对象。

  • Target object:目标对象(被代理的对象)

悲观锁和乐观锁的区别

  • 乐观锁:乐观锁认为竞争不总是发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,应有相应的重试逻辑

  • 悲观锁:认为竞争总是发生,每次对某资源进行操作时,都会持有一个独占锁,如 synchronized

响应式编程

响应式编程是一款使用异步数据流编程的响应式编程思想,提供了非阻塞、异步的特性,便于处理异步场景,从而避免回调地狱和突破 Future 的局限性

响应式编程可以理解为:当某一主题发生改变时,观察此主题的观察者就会立刻收到通知并做出一系列响应

单例模式的应用场景

单例模式可以确保一个类只有一个实例,并提供一个全局访问点来获取这个实例

如:在应用程序里,日志记录器负责将程序运行过程中的信息记录下来,这些信息可能会被写入文件、数据库或者发送到远程服务器。为了保证日志信息的一致性和避免资源竞争,通常会用单例模式。确保整个应用程序只使用一个实例,避免数据错误或资源浪费

工厂模式

核心思想是将对象的创建和使用分离,把对象创建逻辑封装在一个工厂类中,这样在使用对象时,无需关心对象的具体创建过程,只需向工厂请求获取所需对象即可

优势:让代码的可维护性和可扩展性更强,可以根据不同的需求创建不同类型的对象,当需要添加新的对象类型时,只需修改或扩展工厂类

  • 简单工厂模式:工厂模式的基础版本,定义了一个工厂类,该类包括一个创建对象的方法,根据传入的参数来决定创建哪种类型的对象

    // 汽车接口
    interface Car {
    void drive();
    }

    // 轿车类
    class Sedan implements Car {
    @Override
    public void drive() {
    System.out.println("Driving a sedan.");
    }
    }

    // SUV 类
    class SUV implements Car {
    @Override
    public void drive() {
    System.out.println("Driving an SUV.");
    }
    }

    // 汽车工厂类
    class CarFactory {
    public static Car createCar(String type) {
    if ("sedan".equalsIgnoreCase(type)) {
    returnnew Sedan();
    } elseif ("suv".equalsIgnoreCase(type)) {
    returnnew SUV();
    }
    returnnull;
    }
    }

    // 测试代码
    publicclass SimpleFactoryExample {
    public static void main(String[] args) {
    Car sedan = CarFactory.createCar("sedan");
    sedan.drive();

    Car suv = CarFactory.createCar("suv");
    suv.drive();
    }
    }
  • 工厂方法模式:将对象的创建逻辑延迟到子类中实现,每个具体的子类负责创建特定类型的对象。定义了一个抽象的工厂类,其中包含一个抽象的创建对象的方法,具体的创建逻辑由子类实现

    // 汽车接口
    interface Car {
    void drive();
    }

    // 轿车类
    class Sedan implements Car {
    @Override
    public void drive() {
    System.out.println("Driving a sedan.");
    }
    }

    // SUV 类
    class SUV implements Car {
    @Override
    public void drive() {
    System.out.println("Driving an SUV.");
    }
    }

    // 抽象工厂类
    abstract class CarFactory {
    public abstract Car createCar();
    }

    // 轿车工厂类
    class SedanFactory extends CarFactory {
    @Override
    public Car createCar() {
    return new Sedan();
    }
    }

    // SUV 工厂类
    class SUVFactory extends CarFactory {
    @Override
    public Car createCar() {
    return new SUV();
    }
    }

    // 测试代码
    public class FactoryMethodExample {
    public static void main(String[] args) {
    CarFactory sedanFactory = new SedanFactory();
    Car sedan = sedanFactory.createCar();
    sedan.drive();

    CarFactory suvFactory = new SUVFactory();
    Car suv = suvFactory.createCar();
    suv.drive();
    }
    }
  • 抽象工厂模式:提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。允许客户端使用抽象的接口来创建一组相关的对象,而不需要关心这些对象的具体实现。

    // 汽车接口
    interface Car {
    void drive();
    }

    // 轮胎接口
    interface Tire {
    void roll();
    }

    // 宝马汽车类
    class BMWCar implements Car {
    @Override
    public void drive() {
    System.out.println("Driving a BMW car.");
    }
    }

    // 宝马轮胎类
    class BMWTire implements Tire {
    @Override
    public void roll() {
    System.out.println("BMW tire is rolling.");
    }
    }

    // 奔驰汽车类
    class BenzCar implements Car {
    @Override
    public void drive() {
    System.out.println("Driving a Benz car.");
    }
    }

    // 奔驰轮胎类
    class BenzTire implements Tire {
    @Override
    public void roll() {
    System.out.println("Benz tire is rolling.");
    }
    }

    // 抽象工厂接口
    interface CarFactory {
    Car createCar();
    Tire createTire();
    }

    // 宝马工厂类
    class BMWFactory implements CarFactory {
    @Override
    public Car createCar() {
    returnnew BMWCar();
    }

    @Override
    public Tire createTire() {
    returnnew BMWTire();
    }
    }

    // 奔驰工厂类
    class BenzFactory implements CarFactory {
    @Override
    public Car createCar() {
    returnnew BenzCar();
    }

    @Override
    public Tire createTire() {
    returnnew BenzTire();
    }
    }

    // 测试代码
    publicclass AbstractFactoryExample {
    public static void main(String[] args) {
    CarFactory bmwFactory = new BMWFactory();
    Car bmwCar = bmwFactory.createCar();
    Tire bmwTire = bmwFactory.createTire();
    bmwCar.drive();
    bmwTire.roll();

    CarFactory benzFactory = new BenzFactory();
    Car benzCar = benzFactory.createCar();
    Tire benzTire = benzFactory.createTire();
    benzCar.drive();
    benzTire.roll();
    }
    }

哈希算法和加密算法的区别

  • 哈希算法:散列算法,把任意长度的输入数据通过特定的哈希函数转换为固定的长度输出,这个输出值就是哈希值,也称为摘要。哈希函数是确定性的,相同的输入始终会产生相同的输出,并且计算过程是单向的,很难从哈希值反推原始输入数据。常见的哈希算法 - MD5SHA-1SHA-256

  • 加密算法:是将明文数据经过特定的加密密钥和加密算法转换为密文,在需要的时候,再使用对应的解密密钥和解密算法将密文还原为明文。加密过程是可逆的,前提是拥有正确的密钥。加密算法分为对称加密算法和非对称加密算法。

  • 对称加密算法:DESAES,加密和解密使用相同的密钥

  • 非对称加密算法:RSAECC,使用公钥加密,私钥解密

kafka 的选举机制

Kafka 的选举包括:Kafka Controller 的选举,Partition Leader 的选举, Consumer Group Coordinator (消费组协调者)的选举

Kafka Controller 的选举过程:

  • 启动竞争:集群启动时,各个 Broker 节点会尝试在 Zookeeper 中创建一个临时节点 /controllerZooKeeper 是一个分布式协调服务,可保障只有一个节点能成功创建该节点。

  • 确定控制器:成功创建 /controller 节点的 Broker 会成为控制器,并在该节点中记录自身的 Broker ID 等信息

  • 故障处理:若控制器所在节点发生故障,其创建的临时节点会自动消失。其他 Broker 监听到这一变化后,会重新竞争创建 /controller 节点,从而选出新的控制器

Partition Leader 的选举:

  • 控制器主导:分区首领选举由控制器负责。控制器会维护分区的状态信息,当检测到首领副本不可用时,会触发选举流程

  • 优先副本选择Kafka 优先选择分区的优先副本作为首领。优先副本是在分区创建时指定的第一个副本

  • 存活副本选择:若优先副本不可用,控制器会从同步副本集合(ISR)中选择一个副本作为新的首领。同步副本是指定与首领副本保持同步的副本

  • 元数据更新:选举出新首领后,控制器会更新集群元数据,通知其他 Broker 新的首领信息

Consumer Group Coordinator(消费组协调者)的选择:

  • 协调器确定:消费者组中的消费者在启动时,会向 Kafka 集群发送请求, Kafka 根据消费者组 ID 的哈希值选择一个 Broker 作为协调器

  • 注册与通知:消费者向选定的协调器注册自己,协调器会维护消费者组的成员信息,并将分区分配方案通知给各个消费者。

  • 故障处理:若协调器所在的 Broker 故障,消费者会收到通知,重新发起协调器选举

Redis 的持久化机制

Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制。这个机制会把数据存储到磁盘,这样在 Redis 重启后就能从磁盘中恢复原有数据。

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入一个文件里

    • Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复

    • Redis 提供了 3 种写回硬盘的策略:

      • Always:每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘

      • Everysec:每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区数据写回硬盘

      • No:不由 Redis 控制写回时机,转交给操作系统控制写回时机,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再有操作系统决定何时将缓冲区内容写回

  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘。提供了两个命令来生成 RDB 文件

    • save:会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件时间太长,会阻塞主线程

    • bgsave:会创建一个子进程来生成文件,可避免主线程阻塞

AOF 和 RDB 优缺点

AOF:

  • 优点:首先,AOF 提供了更好的数据安全性,因为它默认每接收到一个写命令就会追加到文件末尾。即使 Redis 服务器宕机,也只会丢失最后一次写入前的数据。其次,AOF 支持多种同步策略(如 everysecalways 等),可以根据需要调整数据安全性和性能之间的平衡。同时,AOF 文件在 Redis 启动时可以通过重写机制优化,减少文件体积,加快恢复速度。并且,即使文件发生损坏,AOF 还提供了 redis-check-aof 工具来修复损坏的文件。

  • 缺点:因为记录了每一个写操作,所以 AOF 文件通常比 RDB 文件更大,消耗更多的磁盘空间。并且,频繁的磁盘 IO 操作(尤其是同步策略设置为 always 时)可能会对 Redis 的写入性能造成一定影响。而且,当某个文件体积过大时,AOF 会进行重写操作,AOF 如果没有开启 AOF 重写或者重写频率较低,恢复过程可能较慢,因为它需要重放所有的操作命令。

RDB:

  • 优点: RDB 通过快照的形式保存某一时刻的数据状态,文件体积小,备份和恢复的速度非常快。并且,RDB 是在主线程之外通过 fork 子进程来进行的,不会阻塞服务器处理命令请求,对 Redis 服务的性能影响较小。最后,由于是定期快照,RDB 文件通常比 AOF 文件小得多。

  • 缺点: RDB 方式在两次快照之间,如果 Redis 服务器发生故障,这段时间的数据将会丢失。并且,如果在 RDB 创建快照到恢复期间有写操作,恢复后的数据可能与故障前的数据不完全一致

布隆过滤器

布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。

布隆过滤器会通过 3 个操作完成标记:

  • 使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
  • 将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
  • 将每个哈希值在位图数组的对应位置的值设置为 1;

查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据

CAP 理论

CAP 原则又称 CAP 定理, 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性), 三者不可得兼

  • 一致性(C) : 在分布式系统中的所有数据备份, 在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)

  • 可用性(A): 在集群中一部分节点故障后, 集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)

  • 分区容忍性(P): 以实际效果而言, 分区相当于对通信的时限要求. 系统如果不能在时限内达成数据一致性, 就意味着发生了分区的情况, 必须就当前操作在 C 和 A 之间做出选择

索引的建立怎么考虑

什么时候适用索引?

  • 字段有唯一性限制的,比如商品编码;

  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。

  • 经常用于 GROUP BYORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

什么时候不需要创建索引?

  • WHERE 条件,GROUP BYORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。

  • 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。

  • 表数据太少的时候,不需要创建索引; 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于需要维护索引的有序性,有额外的维护成本。

MySQL 页大小

MySQL 的页( Page )大小通常是 16KB( InnoDB 引擎默认),但可以通过 innodb_page_size 参数调整为 4KB、8KB16KB。页是 MySQL 物理存储的基本单位,所有的数据、索引等都存储在页中。

Es 底层原理

Spring Boot 解决循环依赖

Spring Boot 三级缓存

线程题总结

· 6 min read
Happlay71
一个渴望成为技术大佬的小白

java 开启线程有哪几种方式

在 Java 中,开启线程有多种方式,主要包括以下几种:

1. 继承 Thread

继承 Thread 类并重写 run() 方法是最基本的线程创建方式。

示例:

class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务
System.out.println("Thread is running...");
}
}

public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
  • 优点:代码简单,直观。
  • 缺点:无法继承其他类,因为 Java 不支持多重继承。这样会限制类的灵活性。

2. 实现 Runnable 接口

实现 Runnable 接口是 Java 中推荐的线程创建方式,尤其适用于需要实现多线程功能的类已经继承了其他类的情况。Runnable 接口只包含一个方法:run()

示例:

class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的任务
System.out.println("Runnable thread is running...");
}
}

public class ThreadExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
  • 优点:使用更灵活,可以实现接口并继承其他类。
  • 缺点:需要通过 Thread 类来启动线程。

3. 实现 Callable 接口(适用于任务有返回值的情况)

Callable 接口是 Java 5 引入的,可以在多线程中返回结果。与 Runnable 接口不同的是,Callable 可以返回值,并且可以抛出异常。

示例:

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程任务,返回值
System.out.println("Callable thread is running...");
return 100;
}
}

public class ThreadExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(myCallable);
Integer result = future.get(); // 获取结果
System.out.println("Result from callable: " + result);
executorService.shutdown();
}
}
  • 优点:能够返回结果,可以处理异常。
  • 缺点:需要使用 ExecutorService 来管理线程,稍微复杂一些。

4. 使用 Lambda 表达式(适用于实现 RunnableCallable 接口)

Java 8 引入了 Lambda 表达式,允许简化线程的创建和任务的定义。

示例:

public class ThreadExample {
public static void main(String[] args) {
// 使用 Lambda 表达式创建线程
Runnable runnable = () -> System.out.println("Lambda thread is running...");
Thread thread = new Thread(runnable);
thread.start();
}
}
  • 优点:代码简洁,减少了冗余代码。
  • 缺点:仅限于 Java 8 及以上版本,且通常适用于实现简单的任务。

5. 使用 ExecutorService(线程池)

ExecutorService 提供了一种更高级的线程管理方法,允许开发者在不直接管理线程的情况下执行任务。使用线程池能有效地减少线程创建的开销,并通过线程池来复用线程。

示例:

import java.util.concurrent.*;

public class ThreadExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(() -> {
// 线程任务
System.out.println("Task is running in thread pool...");
});
executorService.shutdown(); // 关闭线程池
}
}
  • 优点:提高线程的利用率,减少线程创建的开销,避免了线程过多的情况。
  • 缺点:使用线程池时需要进行一些额外的管理和配置。

6. 使用 ForkJoinPool(适用于任务分解和并行计算)

ForkJoinPool 是 Java 7 引入的一种线程池类型,专门用于处理大量的小任务,通常用于并行计算场景。它通过任务分解和合并来优化计算资源的利用。

示例:

import java.util.concurrent.*;

public class ForkJoinExample {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(() -> {
System.out.println("Task executed using ForkJoinPool");
});
forkJoinPool.shutdown(); // 关闭线程池
}
}
  • 优点:高效的用于大规模任务的并行计算,特别适合在 CPU 密集型任务中使用。
  • 缺点:适用于特定的场景,不是所有类型的任务都适合使用 ForkJoinPool

总结:

  1. 继承 Thread:适用于简单的线程创建,但受限于 Java 单继承的限制。
  2. 实现 Runnable 接口:适合大多数多线程任务,且可以继承其他类。
  3. 实现 Callable 接口:适合需要返回值或者处理异常的多线程任务。
  4. Lambda 表达式:简化代码,适合简单的任务创建。
  5. ExecutorService(线程池):更为高效、灵活,适合执行大量任务并复用线程。
  6. ForkJoinPool:适合并行计算任务,优化计算资源利用。

选择哪种方式取决于你的具体需求,如任务是否有返回值、是否需要线程池管理等。