上海回合肥年初面试第四天😐

135 阅读19分钟

视频面试 遇到的面试官都不爱开摄像头蛮
java开发工程师 10-15K

【任职要求】
1、本科以上学历,3年以上Java开发经验:
2、熟悉J2EE 技术架构体系,语言基础知识扎实,有00P思想;熟悉Spring微服务等主流开源框架,包 括 SpringBoot、SpringCloud、Mybatis等,熟悉微服务运行、监控、安全等相关知识;熟悉常用设计模 式、主流的SSH框架等:
3、熟悉常用的中间件及性能调优,包括Redis、Kafka、Naco,Elasticsearch 等;
4、熟练使用常用的关系型数据库,如Mysql、oracle、达梦等:
【岗位职责】
1、负责项目/产品的系统开发和第三方接口对接联调等工作;
2、参与产品需求分析和评审,能从技术专业的角度提供评审意见;
3、根据产品需求和产品设计,完成功能开发和自测:
4、根据产品需求,编写技术相关的文档:
5、对线上问题进行跟踪和修复,推动问题及时合理地解决

1.自我介绍

2.介绍一下cms和g1垃圾回收器功能原理区别,并根据业务选择合适的垃圾回收器

​​CMS​​:适合中小堆、低延迟场景(JDK 8 及之前),但需警惕碎片和 Full GC 风险。
​​G1​​:适合大堆、可预测停顿场景(JDK 9+ 默认),综合性能更优。
​​业务决策​​:根据堆大小、延迟敏感度、CPU 核心数选择,优先考虑 G1(除非有特殊兼容性需求)。


选择 CMS 的场景(JDK 8 及之前)​​
​​低延迟优先​​:如金融交易系统、实时 Web 服务。
​​堆内存较小​​(≤4GB),且老年代对象存活率低。
​​注意​​:需监控 Full GC 频率,避免内存碎片问题。

​​选择 G1 的场景(JDK 9+ 推荐)​​
​​大内存应用​​(堆 ≥ 4GB),尤其是多核 CPU 环境。
​​可预测停顿需求​​:如微服务、在线事务处理(OLTP)。
​​混合工作负载​​:同时存在短生命周期对象和长生命周期对象。

​​其他替代方案​​
​​ZGC/Shenandoah​​:适用于超大堆(TB 级)和亚毫秒级停顿(JDK 11+)。
​​Parallel GC​​:追求极致吞吐量(如批处理、数据分析)。

3.怎么设计一个水平可扩展的分布式系统

微服务架构
负载均衡
分库分表
异步消息解耦

4.系统的限流怎么做的


限流算法
计数器算法(固定窗口)​
滑动窗口算法​
漏桶算法(Leaky Bucket)​
令牌桶算法(Token Bucket)​

**工具实现​**​:
-   Redis + Lua(动态计算令牌生成)。
-   Sentinel(阿里开源限流组件)。

5.分布式系统的链路追踪

skywalking

6.mysql sql调优

explain 
索引key key_len 
涉及行数 rows

7.表结构设计优化有哪些

选择合适的数据类型 int char varchar
列太多做拆分
设置合适的索引

分区分表
分区:对于大表,可以通过分区技术将表的数据分布到不同的物理存储上。分区是根据某个字段的值来拆分数据,这样可以提高查询效率。
分表:当表的记录数达到一定程度时,单个表可能会影响性能。此时可以根据某些字段(如时间、ID等)将表进行水平拆分。

复杂的多表连接(JOIN)会消耗大量的计算资源,因此应该尽量避免在表设计中产生大量的 JOIN 操作。

数据冗余与备份
冗余数据:在某些情况下,适当的数据冗余可以提高查询性能。例如,存储数据的摘要信息或者定期计算统计数据,避免频繁的实时计算。
定期备份:设计表时要考虑如何定期备份数据,确保数据不会丢失。

8.数据库读写分离有做过么

 ​**​主从复制​**​:
    -   主库(Master)处理所有写操作,并将数据同步到多个从库(Slave)。
    -   从库复制主库的 binlog 日志,实现数据一致性(异步或半同步复制)。

​**​动态数据源路由​**​:
    -   在 Java 代码中,根据 SQL 类型(读/写)自动选择主库或从库的数据源。
    -   通过 AOP 或拦截器动态切换数据源。

9.主库和从库表结构是一样的么

10.什么时候查从库,什么时候查主库

11.分库分表策略

1. 水平分表(Sharding)
水平分表指将一张表中的数据根据某些规则拆分成多张表,每张表的数据相对独立,表与表之间的结构一致,主要是减少每个表的数据量,从而提高查询效率。
常见的水平分表策略:
按范围分表(Range-based Sharding):根据某个字段(如时间、ID等)的范围将数据划分到不同的表中。比如根据日期范围将数据分到不同的表中。
按哈希分表(Hash-based Sharding):对某个字段进行哈希处理,然后根据哈希值将数据分配到不同的表中。这种方式适合数据量大且访问比较均匀的场景。
按地理位置分表:根据地理位置等属性进行分表,常用于电商、物流等业务场景。

2. 垂直分表
垂直分表指将一个表的不同列拆分到不同的表中,通常是根据访问频率和使用场景,将经常访问的字段和不常访问的字段分别存储在不同的表中,从而减小单表的宽度,提高查询效率。
例如:
将用户的基本信息存储在一个表中,用户的账户信息、支付记录存储在另一个表中。
对于一个包含大量列的表,可以按功能领域对其进行拆分。


分库(Sharding)
分库是指将数据分布到多个数据库实例中。分库不仅可以分表,还可以将一个表分布到不同的数据库中,通常用于解决单一数据库的性能瓶颈。
常见的分库策略:
按范围分库:根据某些规则(如时间段或ID范围等)将数据存储到不同的数据库中。
按业务分库:根据业务模块或业务领域将数据分到不同的数据库中。例如,订单数据存储在一个数据库中,用户数据存储在另一个数据库中。
按地区分库:根据地理区域或访问的地域分配数据到不同的数据库中,这对于跨地区、大规模的应用非常有用。

12.分库分表怎么确定ID唯一

雪花算法
UUID
自增ID+分库分表规则
数据库自增ID + 库表前缀
全局唯一ID服务

13.灰度发布

14.负载均衡策略

1. 轮询(Round Robin)
2. 加权轮询(Weighted Round Robin)
3. 最少连接(Least Connections)
4. 加权最少连接(Weighted Least Connections)
5. IP 哈希(IP Hash)
6. 随机(Random)
7. 最优响应时间(Least Response Time)
等等

15.缓存穿透、击穿、雪崩

16.redis数据类型

string list hash set zset bitmap

17.spring ioc aop

18.设计模式、工厂模式策略模式有使用过么

19.死锁是什么 怎么排查

  1. 分析死锁的代码路径: 通过查看代码中各个线程如何申请锁,可以分析出是否存在死锁的潜在风险。常见的死锁来源通常是两个线程在不同顺序请求多个锁。例如:

    • 线程 A 获取锁 A 后,再请求锁 B;
    • 线程 B 获取锁 B 后,再请求锁 A;

    如果线程 A 持有锁 A 等待锁 B,而线程 B 持有锁 B 等待锁 A,那么就会发生死锁。

    解决方法:尽量避免多个锁的嵌套,或者保持锁的获取顺序一致(即所有线程都按相同的顺序请求锁),从而避免死锁的发生。

  2. 使用死锁检测工具

    • JVM 自带工具:如 jstack,可以在 Java 程序运行时通过打印线程堆栈信息,查看各个线程的状态以及它们所持有和等待的锁信息,从而分析是否存在死锁。

      bashCopy Code
      jstack <process_id> > thread_dump.txt
      

      通过分析生成的线程堆栈信息,可以找到死锁所在的线程和锁。死锁的堆栈信息会显示出线程间的等待关系。

    • 线程转储分析:在程序执行时,手动触发线程转储(通过 Thread.getAllStackTraces() 或一些日志工具),然后查看各线程是否有处于等待状态并且互相等待锁。

  3. 启用 JVM 的死锁监控

    • 在 Java 中,可以使用 -XX:+PrintDeadlock 参数来启用死锁检测,当 JVM 检测到死锁时,它会自动打印出死锁的信息。

      例如:

      bashCopy Code
      java -XX:+PrintDeadlock -jar your_program.jar
      

预防死锁的方法

  1. 加锁顺序一致性:确保所有线程在获取多个锁时,按照相同的顺序加锁。例如,如果有两个锁 AB,线程 1 获取 A 后再获取 B,那么线程 2 也应当按相同顺序加锁。
  2. 锁的粒度尽量小:避免长时间持有锁,减少锁竞争的概率,尽量让锁的持有时间短。
  3. 避免锁嵌套:尽量避免一个线程持有多个锁,避免出现循环依赖的情况。
  4. 使用超时机制:在请求锁时设置超时时间,避免线程永远阻塞等待锁的释放。

20.有遇到过比较复杂的线程问题

21.threadlocal是什么,使用场景

记录当前操作线程登录用户信息
日志记录
数据库连接(或其他资源的共享管理)
用户会话(Session)
线程安全的缓存

描述

ThreadLocal 是 Java 中用于实现线程局部变量的类。它能够为每个线程提供独立的变量副本,使得每个线程都能有一个单独的值,互不干扰。ThreadLocal 类中的变量每个线程都可以持有自己独立的值,而不会被其他线程共享。这就意味着同一个线程在不同的调用中可以访问到相同的变量副本,而其他线程则无法访问或修改该线程的副本。

ThreadLocal 的基本原理

ThreadLocal 为每个线程提供了一个独立的存储空间,线程访问 ThreadLocal 变量时,会返回当前线程的副本,而不会影响其他线程的副本。每个线程都可以在自己的 ThreadLocal 变量中存储数据,这些数据对于其他线程是不可见的。

ThreadLocal 的优缺点

优点:

  • 线程隔离:每个线程的 ThreadLocal 变量副本独立,线程间互不干扰。
  • 性能ThreadLocal 提供了非常高效的线程隔离机制,避免了多线程同步的开销。
  • 简洁的代码:在不需要显式同步的情况下,实现线程局部变量的存取。

缺点:

  • 内存泄漏ThreadLocal 的变量是与线程生命周期相关的,如果线程池中的线程没有及时销毁,可能会导致内存泄漏,因为 ThreadLocal 持有对线程的引用,造成无法回收。

    • 为了避免内存泄漏,在不再使用 ThreadLocal 变量时,可以显式地调用 remove() 方法释放资源。
    javaCopy Code
    threadLocalVar.remove();
    
  • 可维护性差:如果滥用 ThreadLocal,可能导致代码可读性和可维护性差,尤其是在不清楚线程副本的作用范围时。

22.线程和进程

1. 进程(Process)​

  • ​定义​​:进程是程序的 ​​一次执行实例​​,是操作系统资源分配和调度的基本单位。

  • ​特点​​:

    • ​独立内存空间​​:每个进程拥有独立的代码、数据、堆栈等资源,进程间数据隔离。
    • ​高开销​​:创建、切换进程需要分配/回收资源,开销较大。
    • ​安全性高​​:进程崩溃不会直接影响其他进程。
  • ​通信方式​​:需要复杂机制(如管道、共享内存、消息队列)。

  • ​例子​​:打开一个浏览器,每个标签页可能是一个独立进程(现代浏览器设计)。

​2. 线程(Thread)​

  • ​定义​​:线程是进程内的 ​​最小执行单元​​,是CPU调度和分派的基本单位。

  • ​特点​​:

    • ​共享内存​​:同一进程内的线程共享代码、数据、堆等资源。
    • ​低开销​​:创建、切换线程速度快,资源消耗少。
    • ​共享与竞争​​:线程间可直接通信,但需处理同步问题(如锁、信号量)。
  • ​通信方式​​:直接读写共享变量。

  • ​例子​​:浏览器中一个标签页(进程)内多个标签页的渲染、网络请求可能通过多线程实现。

对比项​​进程​​线程​
​资源分配​操作系统分配资源(独立内存)共享进程资源(同一内存空间)
​调度开销​大(需切换上下文、资源隔离)小(共享资源,切换更快)
​容错性​高(崩溃不影响其他进程)低(线程崩溃可能导致整个进程终止)
​适用场景​隔离性任务(如浏览器标签页)频繁交互任务(如多任务处理)

23.synchorized原理
synchronized 是 Java 中用于实现同步的一种关键字,它可以保证在同一时刻,只有一个线程能执行某个特定的代码块,从而避免多个线程在并发执行时发生竞争条件(race condition)。

synchronized 的底层原理

synchronized 在 Java 中有两种常见的使用方式:

  1. 同步方法:通过 synchronized 修饰方法,使得该方法在同一时刻只能被一个线程访问。
  2. 同步代码块:通过 synchronized 修饰代码块,使得该代码块在同一时刻只能被一个线程访问。

1. 同步方法的底层原理

当使用 synchronized 修饰一个方法时,JVM 会在该方法执行时自动对其加锁。其底层是通过 对象监视器(Object Monitor) 来实现的,通常称为 锁(Lock)

工作流程:
  • 每个 Java 对象都有一个与之关联的 监视器锁(monitor lock),即对象的锁。
  • 当一个线程调用同步方法时,它会首先尝试获得该对象的锁(monitor)。
  • 如果没有其他线程持有该锁,则当前线程可以继续执行该方法。
  • 如果有其他线程持有该锁,则当前线程会被挂起,直到锁被释放。
  • 锁释放的时机通常是在方法执行完毕后,或者发生异常时。

2. 同步代码块的底层原理

同步代码块允许我们在方法内部指定一个特定的对象作为锁。与同步方法类似,当一个线程执行同步代码块时,它会尝试获得指定对象的锁,成功后继续执行代码块,执行完后释放锁。

代码示例:
javaCopy Code
public void method() {
    synchronized(this) {
        // 被锁住的代码块
    }
}

此时,线程会尝试获取 当前对象(this) 的锁。若当前线程获取锁成功,则执行代码块;如果锁已经被其他线程持有,当前线程会被阻塞,直到锁被释放。

3. 锁的实现机制 - Monitor

Java 的 synchronized 锁实际上是依赖于 Monitor(监视器)来实现的。每个 Java 对象都有一个与之相关联的 监视器锁,这个监视器锁是由 JVM 提供的底层机制。

  • 在 JVM 中,每个对象都有一个 monitor,当进入 synchronized 修饰的代码块时,线程会尝试获取该对象的 monitor,而这个过程是由 JVM 内部的 对象监视器来完成的。
  • 当线程获取到锁(monitor)时,它就能执行同步代码块,其他线程必须等待锁被释放。
  • 一旦同步代码块执行完毕,锁被释放,其他线程可以继续竞争该锁。

4. 底层实现(JVM层面)

在 JVM 内部,synchronized 的实现会依赖于操作系统提供的 互斥锁(mutex)或 信号量。以下是 JVM 对 synchronized 的常见实现:

  1. 对象头(Object Header)

    • 在每个对象的内存布局中,JVM 会为每个对象维护一个对象头(Object Header),其中存储了与该对象相关的锁信息。synchronized 使用这个锁标记来控制线程的同步。
    • 对象头中有两个重要的部分:Mark Word 和 Class Pointer。Mark Word 存储对象的锁状态,如锁的拥有者、锁的状态等。
  2. 偏向锁(Biased Locking)

    • Java 的 synchronized 在 JDK 1.6 以后引入了 偏向锁,当一个线程频繁访问某个同步代码块时,JVM 会将锁的状态设置为偏向锁。这样,后续相同的线程将不再需要获取锁,从而提高性能。
    • 如果不同线程竞争同一把锁,则偏向锁会升级为 轻量级锁
  3. 轻量级锁(Lightweight Locking)

    • 轻量级锁主要用于避免无竞争的情况下的线程阻塞。当多个线程尝试访问同一个同步块时,JVM 会采用 自旋锁 的方式来尝试获取锁,而不是直接进入阻塞状态。自旋锁是一种线程在尝试获取锁时不断循环检查锁状态的机制。
  4. 重量级锁(Heavyweight Locking)

    • 当锁的竞争较为激烈时,锁会升级为重量级锁,JVM 会通过操作系统的原子操作来实现互斥。这时,线程会被挂起并进入操作系统的阻塞队列,直到获得锁。
  5. 锁的升级机制

    • 从偏向锁到轻量级锁,再到重量级锁的升级过程,JVM 会根据锁的竞争情况自动调整锁的类型。

5. 死锁的情况

synchronized 在不当使用时可能会导致 死锁。死锁发生在两个或多个线程互相持有对方所需要的锁,导致线程永远等待下去。为了避免死锁,可以遵循以下规则:

  • 保证锁的获取顺序一致。
  • 使用 tryLock 或 Timeout 的方式避免无限期等待。

总结:

  • synchronized 在 Java 中通过 Monitor 来实现线程同步,它依赖于对象头中的锁标记来管理锁状态。
  • 通过 偏向锁轻量级锁重量级锁 等多种锁策略,JVM 尝试优化同步的性能。
  • 在多线程环境中,synchronized 提供了一种简单且高效的方式来确保数据一致性,但需要谨慎使用以避免死锁等问题。

24.synchorized使用场景

多线程修改同一变量
单例模式中实现线程安全

-   **优先使用 `synchronized`​**​:  
    适合大多数简单同步场景(如单例、计数器、临界区保护)。
-   ​**​避免复杂场景​**​:  
    需要超时、可中断、公平锁时,改用 `ReentrantLock`。

25.redis集群

26.nacos、apollo、eureka区别、选型

nacos性能更好 吞吐量更高 优先选用

27.CAS是什么、使用场景

28.reentrantLock和synchorized区别,优先使用哪些

29.线程池核心参数,队列一般选用哪些

核心参数​

​参数​​作用​​配置建议​
corePoolSize核心线程数,即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut)。根据任务类型和系统资源设定,通常为 CPU 核心数的 1~2 倍(CPU 密集型任务)。
maximumPoolSize最大线程数,线程池允许的最大并发线程数。需结合任务类型和系统资源(如内存、CPU)设定,避免过多线程导致资源竞争。
keepAliveTime非核心线程空闲存活时间,超过该时间会被回收。根据任务到达频率设定,短任务可设较短(如 60s),长任务可设较长(如 300s)。
unitkeepAliveTime 的时间单位(如 TimeUnit.SECONDS)。根据实际需求选择(秒、毫秒等)。
workQueue任务队列,用于存放等待执行的任务。根据任务特性选择(无界队列、有界队列、同步移交队列)。
threadFactory线程工厂,用于创建线程(可自定义线程名称、优先级等)。建议自定义线程名称(如 new CustomThreadFactory("MyThreadPool"))。
handler拒绝策略,当队列满且线程数达最大值时的任务处理策略。根据场景选择(抛出异常、丢弃任务、调用者线程执行等)。
---------------------------
​队列类型​​特点​​适用场景​
​无界队列​无固定容量(如 LinkedBlockingQueue),任务无限堆积,可能导致 OOM。任务量可控、执行时间短(如 Web 服务器处理 HTTP 请求)。
​有界队列​固定容量(如 ArrayBlockingQueue),队列满时触发拒绝策略。需严格控制内存使用(如批处理任务、高并发场景)。
​同步移交队列​不存储任务,直接将任务交给线程处理(如 SynchronousQueue)。高吞吐场景,任务直接传递(如线程池需快速响应,避免任务堆积)。
​优先级队列​按优先级排序任务(如 PriorityBlockingQueue),需实现 Comparable 接口。任务需按优先级处理(如支付回调、紧急任务优先执行)。

队列选择建议​

  • ​高吞吐、低延迟场景​​:
    使用 ​SynchronousQueue​(如 Executors.newCachedThreadPool()),任务直接传递给空闲线程,避免队列堆积。
  • ​任务量波动大​​:
    使用 ​​有界队列​​(如 ArrayBlockingQueue),防止突发流量导致内存溢出。
  • ​任务需公平性​​:
    使用 ​LinkedBlockingQueue​(FIFO),保证任务按提交顺序执行。
  • ​任务优先级差异​​:
    使用 ​PriorityBlockingQueue​,结合 Comparator 定义优先级。

拒绝策略(handler)​

​策略​​行为​​适用场景​
AbortPolicy​(默认)抛出 RejectedExecutionException 异常。需明确感知任务被拒绝(如金融交易场景)。
CallerRunsPolicy由提交任务的线程执行任务(调用者线程)。平缓限流,避免系统过载(如 Web 服务)。
DiscardPolicy直接丢弃任务,不抛出异常。可丢弃的非关键任务(如日志采集)。
DiscardOldestPolicy丢弃队列中最旧的任务,重新尝试提交当前任务。需优先处理新任务的场景(如实时数据流)。
  • 核心参数​​:根据任务类型(CPU/I/O 密集型)和系统资源动态调整。
  • ​队列选择​​:优先考虑有界队列(防 OOM)+ 合理拒绝策略(保稳定性)。
  • ​配置公式​​:线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均计算时间)
  • ​关键点​​:避免过度配置(浪费资源)或配置不足(性能瓶颈)。

30.如果是一个CPU密集型任务 线程池计算公式是什么

对于 CPU 密集型任务来说,线程池大小的选择非常重要。过多的线程可能会导致过度的上下文切换,降低效率;而过少的线程则可能没有充分利用 CPU 核心的计算能力。

-   对于 CPU 密集型任务,最佳线程池大小通常设置为 **CPU 核心数**。
-   线程数过多会导致上下文切换带来额外的开销,影响性能。

-   CPU 密集型:`线程数 ≈ CPU 核心数 + 1`
-   I/O 密集型:`线程数 ≈ CPU 核心数 * (1 + 平均等待时间 / 平均计算时间)`