参考材料
链接:https://pan.quark.cn/s/2b9bed49a350?pwd=szrK
提取码:szrK
问题:你说你的软件出现了 OOM, 你怎么解决的? 线上 crash 你是用了什么来分析的?
Java heap space:堆内存不足,可能因内存泄漏或超大对象(如未分页的数据库查询)。
Metaspace/PermGen space:类元数据区溢出(如动态生成大量类)。
Unable to create new native thread:线程数超限或栈内存不足。
Direct buffer memory:堆外内存(如NIO的DirectByteBuffer)溢出。
在启动脚本中添加以下参数,生成堆转储(Heap Dump)和GC日志:
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/path/to/dump.hprof \
-XX:+PrintGCDetails -Xloggc:/path/to/gc.log
频繁Full GC或老年代内存持续增长 → 内存泄漏迹象。
GC后内存回收率低 → 堆内存不足或对象生命周期过长。
压力测试:模拟高并发场景,观察内存增长趋势。
监控:Prometheus + Grafana监控堆内存、线程数、GC频率。
合理设置-Xmx/-Xms(避免过大导致系统OOM Killer干预)。
元空间动态调整:-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m。
问题:线程之间是共享内存的, 但是线程之间为什么会出现不同步呢?(多线程可见性) 因为除了内存,数据还会被缓存到 CPU 寄存器和各级缓存中, 当修改一个变量的时候, 可能会先写到缓存, 稍后再更新到
如何解决多线程可见性问题?
解决方案4个: violate, atomic, synchronized, lock
1. volatile关键字
作用:强制每次读写操作直接访问主内存,禁止指令重排序(通过内存屏障实现)。
适用场景:简单状态标志(如boolean flag)。
局限:不保证复合操作(如i++)的原子性。
private volatile boolean flag = false;
2. 同步机制(synchronized/Lock)
原理:
进入同步块前清空工作内存,从主存加载最新值。
退出同步块时将修改刷回主存。
适用场景:需要原子性+可见性的复合操作(如账户余额增减)。
public synchronized void update() {
count++; // 原子操作+可见性
}
3. 原子类(AtomicInteger等) - 就是可以不用Syncronization 然后实现线程安全
原理:基于CAS(Compare-And-Swap)实现无锁原子操作,内部通过CPU指令保证可见性。
优势:高并发下性能优于锁。
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子自增
###关键看这个地方,因为Volatile, 所以先compare再 + Delta
@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
4. ThreadLocal隔离变量
原理:为每个线程创建变量副本,彻底避免共享(空间换时间)。
适用场景:线程独立的上下文(如用户会话)。
private ThreadLocal<Integer> localCount = ThreadLocal.withInitial(() -> 0);
问题:TCP/UDP 的区别,答了连接可靠新和报文的冗余度。又问了下应用场景
TCP:为“数据精准”而生,牺牲速度换可靠,主导Web、邮件、文件等关键业务。
UDP:为“实时响应”而生,牺牲可靠换效率,统治音视频、游戏、物联网等场景。
问题:一个 tcp 报文最大是多少?udp 呢?
IP 层限制:IP 数据报最大长度为 65,535 字节(受 16 位总长度字段限制)。
TCP 实际载荷:需扣除 IP 头部(20 字节)和 TCP 头部(20 字节),因此 TCP 数据部分最大为:
65,535 - 20 - 20 = 65,495 字节。
UDP 长度字段:
UDP 头部中“长度”字段占 16 位,因此整个 UDP 数据报(头部+数据)最大为:
2¹⁶ - 1 = 65,535 字节(64KB)。
问题:tcp 怎么保证完整性 校验和
TCP 校验和不仅计算 TCP 头部和数据部分,还包含一个 伪头部(Pseudo-Header),伪头部由以下字段组成:
源 IP 地址(32 位)
目的 IP 地址(32 位)
协议类型(8 位,固定为 6 表示 TCP)
TCP 报文长度(16 位,指 TCP 头部 + 数据的总长度)
接收端验证
提取伪头部信息(IP 地址、协议类型等)和 TCP 报文。
重新计算校验和,若结果非 0xFFFF,则判定数据损坏,直接丢弃且不发送 ACK,触发发送方超时重传 。
问题:timewait 等待时间内核参数怎么修改
sudo vi /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_max_tw_buckets = 10000
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 10000 65000
sudo sysctl -p
针对JDBC
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(50); // 控制最大连接数
config.setIdleTimeout(600000); // 空闲连接超时时间(毫秒)
针对socket
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); // 必须在 bind() 前调用
serverSocket.bind(new InetSocketAddress(8080));
问题:内核是怎么回收的 soc
Java虚拟机(JVM)的垃圾回收(GC)机制中,Survivor区(包括S0和S1)是年轻代(Young Generation)的重要组成部分,它们与Eden区共同构成了分代垃圾回收模型的关键部分。下面详细解析Survivor区在Java内存回收中的工作原理。
一、Survivor区的基本结构
Survivor区(又称存活区)通常由两个大小相等的区域组成:S0和S1。它们的主要作用是存放从Eden区经过Minor GC后存活下来的对象。这种设计基于"复制算法",通过空间交换来减少内存碎片。
Survivor区的特点:
大小相等:S0和S1通常被设置为相同大小
交替使用:任何时候只有一个Survivor区处于活跃状态
年龄计数:对象在Survivor区之间每次复制转移,年龄计数器会增加1
二、Survivor区的回收过程
1. 对象分配初始阶段
新创建的对象首先会被分配到Eden区。当Eden区空间不足时,会触发Minor GC。
2. Minor GC的执行流程
标记阶段:GC Roots开始遍历对象图,标记Eden区和当前使用的Survivor区(S0或S1)中的所有存活对象
复制阶段:
将Eden区中存活的对象复制到另一个Survivor区(非当前使用的那个)
将当前Survivor区中存活的对象也复制到另一个Survivor区
清理阶段:清空Eden区和刚刚使用过的Survivor区
3. 对象年龄与晋升
每次Minor GC后,存活对象的年龄会增加1
当对象年龄达到阈值(默认15)时,会被晋升到老年代(Old Generation)
如果Survivor区空间不足存放所有存活对象,部分对象会直接晋升到老年代
三、Survivor区的设计目的
减少老年代GC压力:通过Survivor区的筛选,只有真正长期存活的对象才会进入老年代
优化复制算法效率:通过两个Survivor区的交替使用,避免了内存碎片的产生
对象年龄分级:通过多次Minor GC筛选出真正需要长期存活的对象
问题:问了 Java 的垃圾回收器,介绍了串行(SGC)和 CMS 然后说可
SGC - Serial GC
CMS - Concurrent GC: G1GC 就是CMS
一、串行垃圾回收器(Serial GC)
1. 核心特点
单线程执行:### 垃圾回收时暂停所有应用线程(Stop-The-World, STW),采用单线程完成标记、复制(新生代)和标记-整理(老年代)。
内存碎片少:老年代使用标记-整理算法,减少内存碎片,适合小内存环境。
低复杂度:无多线程同步开销,适合单核CPU或嵌入式设备。
2. 工作流程
新生代回收:
使用复制算法,将 Eden 区和 Survivor 区的存活对象复制到另一 Survivor 区(或直接晋升老年代)。
老年代回收:
标记存活对象 → 整理到内存一端 → 清除剩余空间。
3. 优缺点
优点:
简单高效,适用于堆内存百MB级、无低延迟要求的场景(如命令行工具)。
内存利用率高,碎片少。
缺点:
STW 时间长,高并发场景下性能急剧下降。
无法利用多核CPU资源。
-XX:+UseSerialGC ## 问题:显式启用串行回收器
二、并发标记清除回收器(CMS GC)
1. 设计目标
低延迟:通过并发标记和清除减少 STW 时间,适合响应敏感型应用(如 Web 服务)。
2. 四阶段工作流程
初始标记(STW):
标记 GC Roots 直接关联的对象,耗时极短(约 1ms)。
并发标记:
与用户线程并发执行,遍历对象图标记存活对象,耗时较长(约 50ms)。
-XX:+UseConcMarkSweepGC ## 问题:启用CMS
-XX:CMSInitiatingOccupancyFraction=70 ## 问题:老年代占用70%时触发CMS
-XX:+UseCMSCompactAtFullCollection ## 问题:Full GC时压缩内存
JDK 8及之前:CMS(低延迟)或 Parallel GC(高吞吐)。
JDK 9+:默认 G1,大堆或低延迟需求考虑 ZGC。
问题:SpringBoot 的自动装配啥的
条件化配置(Conditional)
通过条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean)动态决定是否加载某个配置类或 Bean,避免不必要的组件加载。
常见条件注解:
@ConditionalOnClass:类路径存在指定类时生效。
@ConditionalOnMissingBean:容器中不存在指定 Bean 时生效。
@ConditionalOnProperty:配置文件属性满足条件时生效。
约定优于配置
提供默认配置(如内嵌 Tomcat、默认数据源),开发者仅需覆盖必要配置。
依赖驱动的自动配置
根据项目依赖(如 spring-boot-starter-web)自动加载相关配置(如 WebMvcAutoConfiguration)。
问题:3. 谈谈 XSS 的分类和预防
XSS攻击的分类与全面防御策略
XSS(跨站脚本攻击)是Web安全领域最常见且危害性极大的安全漏洞之一,攻击者通过在网页中注入恶意脚本,使得其他用户在浏览网页时执行这些脚本,从而达到窃取用户信息、会话劫持等恶意目的。下面我将从XSS攻击的分类原理和防御措施两方面进行全面阐述。
一、XSS攻击的分类
根据攻击方式和持久性的不同,XSS攻击主要分为三大类型,每种类型具有不同的特点和攻击场景。
1. 反射型XSS(非持久型XSS)
反射型XSS是最常见的XSS攻击形式,其攻击流程具有即时性特点:
攻击原理:恶意脚本作为请求参数(通常通过URL)发送到服务器,服务器未经验证直接将参数内容反射回客户端页面,浏览器解析执行其中的恶意代码
典型场景:搜索框、错误消息页面、表单提交结果页等直接将用户输入显示在页面上的地方
2. 存储型XSS(持久型XSS)
存储型XSS是危害性最大的XSS攻击形式:
攻击原理:恶意脚本被提交并存储在服务器端(如数据库、文件系统等),当其他用户访问包含该内容的页面时,脚本从服务器加载并执行
3. DOM型XSS
DOM型XSS是一种纯客户端攻击方式,不依赖服务器响应:
攻击原理:恶意脚本通过修改页面的DOM环境(文档结构、属性等)来执行,攻击载荷不经过服务器处理
二、XSS攻击的防御策略
1. 输入验证与过滤
2. 输出编码
黄金法则:"在将数据输出到不同上下文时进行适当的编码"
3. 内容安全策略(CSP)
CSP是最有效的XSS防御机制之一,通过HTTP头定义资源加载策略
问题:4. CSRF 是什么?怎么预防?
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种网络攻击手段,攻击者通过伪装成受信任用户的请求,利用用户已登录的身份在目标网站上执行非授权操作(如转账、修改密码等)。以下是其核心原理与防御策略的详细解析:
// AJAX请求头添加Token
headers: { 'X-CSRF-Token': token }
优势:Token不可预测,且不存储在Cookie中。
问题:5. SSRF 是什么?怎么预
SSRF 是什么?
SSRF(Server-Side Request Forgery,服务器端请求伪造)是一种由攻击者构造请求,诱导服务器端发起非预期网络请求的安全漏洞。
其核心原理是:服务器端程序在处理用户输入的 URL 或资源地址时,未严格验证合法性,导致攻击者可通过构造恶意地址,让服务器向内网地址、其他服务器或敏感服务(如数据库、Redis、云服务 metadata 接口等)发起请求,从而窃取信息、攻击内网系统或绕过访问控制。
禁用不必要的网络功能:若服务器无需对外发起请求,关闭相关功能(如禁止使用 curl、file_get_contents 等函数)。
降低服务器权限:运行服务器的进程使用低权限账户,避免其拥有访问内网敏感服务(如数据库、Redis)的权限。
隔离内网与外网:通过网络隔离(如防火墙、VPC)限制服务器对校内敏感网段的访问,仅开放必要端口。
问题:java进程间通讯怎么处理?线程间呢?
一、进程间通信(IPC)
进程拥有独立的内存空间,需通过内核或共享资源进行通信。主要方式包括:
1. 基于网络的通信
套接字(Socket)
原理:基于TCP/UDP协议,支持跨主机或本地进程通信。
HTTP/REST API
场景:微服务间通信,标准化协议支持JSON/XML。
2. 基于文件的通信
共享文件
原理:通过读写文件交换数据,适合大数据量场景。
3. 内存映射文件(Memory-Mapped Files)
原理:将文件映射到内存,直接操作内存区域。
4. 基于消息的通信
问题:java有共享池吗?
1. 常量池(Constant Pool)
位置:方法区(JDK7之前) → 元空间(JDK8+)。
作用:
存储编译期生成的字面量(如数字、字符串)和符号引用(类/方法/字段的名称)。
类加载后,符号引用解析为直接地址,存入运行时常量池(Runtime Constant Pool),供JVM指令动态链接使用。
特点:
所有线程共享,避免重复创建相同常量对象。
溢出时抛出 OutOfMemoryError: Metaspace(JDK8+)。
📍 2. 字符串常量池(String Pool)
位置:堆内存(JDK7起)。
作用:
存储字符串字面量(如 String s = "abc"),避免重复创建相同字符串对象。
通过 String.intern() 方法将字符串显式加入池中。
特点:
使用 == 比较时,若引用指向池中同一对象则返回 true。
减少内存开销,提升字符串操作效率。
🧵 3. 线程池(Thread Pool)
位置:堆内存中管理线程对象。
作用:
复用线程,避免频繁创建/销毁线程的开销。
通过 java.util.concurrent.Executors 创建,如:
newFixedThreadPool():固定大小线程池。
newCachedThreadPool():动态调整线程数。
特点:
任务队列管理任务分发,核心线程常驻,非核心线程超时回收。
提升高并发场景下的响应速度和资源利用率。
🔗 4. 连接池(Connection Pool)
位置:堆内存中管理连接对象。
作用:
复用数据库/JMS等网络连接,减少创建连接的耗时(如数据库连接需三次握手)。
实现方式:
受管连接池:由容器(如Tomcat)或服务提供方管理,通过JNDI获取。
自定义连接池:使用Apache Commons Pool等库实现。
特点:
通过 DataSource 接口获取连接,使用后归还而非关闭。
显著提升数据库访问性能,避免连接泄露。
🧩 5. TLAB(线程本地分配缓冲区)
位置:堆内存的Eden区。
作用:
为每个线程划分一小块私有内存区域(默认占Eden 1%),用于快速分配小对象。
避免多线程竞争堆内存分配锁(通过CAS同步)。
特点:
分配行为线程私有,但对象本身仍可被其他线程访问(共享堆)。
大对象直接在堆中分配,TLAB空间不足时触发优化策略(如废弃并重新申请)。
问题:synchronized 和 Lock 的区别
维度 synchronized Lock(如 ReentrantLock)
实现方式 Java 关键字,JVM 层面实现(基于 Monitor 机制) Java API 接口(如 ReentrantLock 基于 AQS)
锁的获取/释放 自动获取和释放(进入/退出同步块或方法时) 需手动调用 lock() 和 unlock()(通常在 finally 中释放)
锁类型 仅支持非公平锁 支持公平锁和非公平锁(通过构造函数设置)
// 定义两个条件变量:非满条件(生产者用)和非空条件(消费者用)
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
notFull.await(); // 释放锁并阻塞,直到notFull被唤醒
notEmpty.signal(); // 唤醒一个等待notEmpty的消费者
比较 violate synchronized, lock, atomic
使用场景:
是否需要线程安全?
├─ 是 → 操作类型?
│ ├─ 单次读写 → volatile
│ ├─ 复合操作 → 是否需要高级控制?
│ │ ├─ 否 → synchronized
│ │ └─ 是 → Lock
│ └─ 原子变量 → Atomic
└─ 否 → 无锁编程(如ThreadLocal)
核心关系与特性对比
volatile
作用:保证可见性和有序性,但不保证原子性。
UML关联:独立节点,与synchronized和Atomic并列。
典型场景:状态标志位(如boolean flag)。
synchronized
作用:通过JVM内置锁实现原子性、可见性和互斥性,支持锁升级优化。
UML关联:继承自并发控制机制,与Lock形成对比。
局限性:非公平、不可中断、单条件队列。
Lock接口
作用:显式锁,提供更灵活的锁控制(如公平锁、可中断、超时机制)。
UML关联:接口实现类ReentrantLock,支持多Condition。
性能:高竞争场景下优于synchronized。
Atomic类
作用:基于CAS实现无锁原子操作,解决synchronized性能瓶颈。
UML关联:依赖Unsafe类的CAS操作,需处理ABA问题。
子类:AtomicInteger、AtomicStampedReference等。
锁的公平性:ReentrantLock可配置公平/非公平,而synchronized仅非公平。
性能权衡:低竞争用synchronized,高竞争用Lock或Atomic。
ABA问题:AtomicStampedReference通过版本号解决。
问题:synchronized 内部实现,偏向锁,轻量锁,重量锁
Synchronized 相当于隐形锁,通过对象头的标志确定锁的状态。其根据多线程的竞争状态启动确定锁的等级
#### 问题:这个总结就很关键
无锁 → 偏向锁:首次同步时偏向当前线程(需启用偏向锁)。
偏向锁 → 轻量级锁:检测到竞争时撤销偏向,通过 CAS 自旋获取锁。
轻量级锁 → 重量级锁:自旋失败后升级,线程进入阻塞队列。
1. 偏向锁(Biased Locking)
适用场景:单线程重复获取同一把锁,无竞争时性能最优。
核心机制:通过对象头记录线程 ID,避免 CAS 操作。
2. 轻量级锁(Lightweight Locking)
适用场景:多线程交替执行同步块,竞争不激烈。
核心机制:通过 CAS 自旋和栈帧锁记录(Lock Record)实现。
###关键看以下 thin lock: 0x0000000350bff018 的部位
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000350bff018 (thin lock: 0x0000000350bff018)
8 4 (object header: class) 0x00000d58
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
3. 重量级锁(Heavyweight Locking)
适用场景:高并发竞争,线程阻塞。
核心机制:依赖操作系统互斥量(Mutex)和 ObjectMonitor。
@startuml Java并发编程核心机制
skinparam nodesep 50
skinparam ranksep 50
package "Java并发控制机制" {
interface 并发控制机制 <<interface>> {
+ 保证线程安全()
}
class volatile {
+ 特性: 可见性、禁止指令重排序
+ 使用场景: 状态标志、单次读写
- 底层: JMM内存屏障
-- 限制 --
- 不保证原子性(如i++)
}
class synchronized {
+ 特性: 原子性、可见性、互斥性
+ 实现方式: 方法级/块级锁
+ 锁升级: 偏向锁→轻量级锁→重量级锁
-- 限制 --
- 非公平锁
- 不可中断
}
interface Lock <<interface>> {
+ lock()
+ unlock()
+ tryLock()
+ lockInterruptibly()
}
class ReentrantLock {
+ 公平锁/非公平锁
+ Condition支持
+ 性能: 高竞争下更优
}
class Atomic {
+ 实现原理: CAS + volatile
+ 子类: AtomicInteger, AtomicLong等
+ ABA问题解决: AtomicStampedReference
}
并发控制机制 <|-- volatile
并发控制机制 <|-- synchronized
并发控制机制 <|-- Lock
并发控制机制 <|-- Atomic
Lock <|-- ReentrantLock
}
note top of volatile
**适用场景**:
1. 单线程写、多线程读的状态标志
2. 禁止指令重排序(如双重检查锁)
end note
note right of synchronized
**锁优化**:
1. 偏向锁:无竞争时直接访问
2. 轻量级锁:CAS自旋
3. 重量级锁:OS互斥量
end note
note bottom of ReentrantLock
**高级功能**:
1. 可中断锁
2. 超时获取锁
3. 多条件变量(Condition)
end note
note left of Atomic
**无锁编程**:
1. 基于CPU的CAS指令
2. 适合计数器等高并发场景
3. 需处理ABA问题
end note
@enduml
问题:Spring 事务是怎么实现的?
Spring 事务的实现基于 AOP(面向切面编程) 和 事务管理器(PlatformTransactionManager) 两大核心机制,通过代理模式动态增强方法行为,统一管理事务的创建、提交和回滚。以下是其实现原理及关键组件的详细解析:
Spring 事务的实现基于 AOP(面向切面编程) 和 事务管理器(PlatformTransactionManager) 两大核心机制,通过代理模式动态增强方法行为,统一管理事务的创建、提交和回滚。以下是其实现原理及关键组件的详细解析:
一、核心实现机制
AOP 代理
代理生成:当类或方法被 @Transactional 注解标记时,Spring 通过 JDK 动态代理(针对接口)或 CGLIB(针对类)生成代理对象。
拦截逻辑:代理对象拦截目标方法调用,根据事务属性(如传播行为、隔离级别)决定是否开启事务,并在方法执行后提交或回滚。
事务拦截器:核心拦截逻辑由 TransactionInterceptor 实现,它调用事务管理器完成事务操作。
事务管理器(PlatformTransactionManager)
核心接口:定义了 getTransaction()、commit()、rollback() 等方法,抽象了不同数据源的事务操作。
常见实现:
###其实就是AOP + JDBC template + transactionTemplate.execute(status -> {} + status.setRollbackOnly()
###demo 代码如下:
DataSourceTransactionManager:用于 JDBC 单数据源事务。
JpaTransactionManager:支持 JPA/Hibernate。
JtaTransactionManager:分布式事务管理。
JdbcConfig config = new JdbcConfig();
DataSource dataSource = config.dataSource();
JdbcTemplate jdbcTemplate = config.jdbcTemplate(dataSource);
TransactionTemplate transactionTemplate = config.transactionTemplate();
transactionTemplate.execute(status -> {
try {
// 获取当前事务连接
Connection conn = DataSourceUtils.getConnection(dataSource);
System.out.println("事务连接ID: " + conn);
jdbcTemplate.update("INSERT INTO CITI.employee VALUES (CITI.employee_seq.NEXTVAL,'李四','李四')");
// 验证是否同一连接
Connection conn2 = DataSourceUtils.getConnection(dataSource);
System.out.println("操作后连接ID: " + conn2); // 应与conn相同, 这样就能 rollback;
jdbcTemplate.update("""
UPDATE CITI.employee
SET name = '012345678901234567890123456789012345678901234567890123456789'
WHERE NAME = '李四'
""");
return null;
} catch (DataAccessException e) {
status.setRollbackOnly(); // 手动回滚
throw e; // 重新抛出异常
}
});
问题:类加载机制?
一、类加载的五个阶段
加载(Loading)
任务:通过类的全限定名获取二进制字节流,将字节流转换为方法区的运行时数据结构,并生成一个java.lang.Class对象作为访问入口。
数据来源:可从本地文件(如JAR包)、网络、动态生成(如动态代理)等获取字节流。
验证(Verification)
目的:确保字节码合法且不会危害JVM安全。
准备(Preparation)
任务:为类变量(static修饰)分配内存并设置默认初始值(如int为0,对象为null)。
注意:final static常量直接赋实际值(如public static final int VALUE = 123)。
解析(Resolution)
任务:将常量池中的符号引用(如类名、方法名)替换为直接引用(内存地址或偏移量)。
延迟解析:部分解析可能在初始化后完成,支持动态绑定。
初始化(Initialization)
触发条件:首次主动使用类时(如new、访问静态字段、反射调用等)。
执行内容:
执行静态变量赋值和静态代码块(<clinit>方法)。
父类优先初始化(继承场景)。
二、类加载器与双亲委派模型
类加载器分类
启动类加载器(Bootstrap):C++实现,加载JAVA_HOME/lib下的核心类(如java.lang.*)。
扩展类加载器(Extension):加载JAVA_HOME/lib/ext下的扩展类。
应用类加载器(Application):加载用户类路径(ClassPath)的类,默认的类加载器。
自定义类加载器:继承ClassLoader,实现热部署或隔离加载(如Tomcat的多Web应用)。
双亲委派模型
机制:子加载器收到请求后,先委派给父加载器,依次向上传递,若父加载器无法完成,子加载器才尝试加载。
优势:
避免重复加载,确保类唯一性(如java.lang.String始终由Bootstrap加载)。
保护核心类不被篡改。
打破场景:如JDBC驱动需通过线程上下文类加载器反向委派。
三、类加载的触发时机
主动引用(触发初始化)
new实例化、访问/修改静态字段(非final)、调用静态方法。
反射调用(如Class.forName())。
初始化子类时父类未初始化。
JVM启动的主类(含main()方法)。
被动引用(不触发初始化)
通过子类引用父类的静态字段(仅初始化父类)。
数组类定义(如MyClass[] arr)。
常量(final static)的引用
问题:一个Class里面到底有什么
private static ClassData parse(Object o, Class klass) {
if (klass.isArray()) {
return new ClassData(o, klass.getName(), klass.getComponentType().getName(), arrayLength(o));
} else {
ClassData cd = new ClassData(o, klass.getName());
Class superKlass = klass.getSuperclass();
cd.isContended = ContendedSupport.isContended(klass);
if (superKlass != null) {
cd.addSuperClassData(klass.getSuperclass());
}
do {
for(Field f : klass.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())) {
cd.addField(FieldData.parse(f));
}
}
cd.addSuperClass(ClassUtils.getSafeName(klass));
} while((klass = klass.getSuperclass()) != null);
return cd;
}
}
private static class Atomic {
// initialize Unsafe machinery here, since we need to call Class.class instance method
// and have to avoid calling it in the static initializer of the Class class...
private static final Unsafe unsafe = Unsafe.getUnsafe(); #### 问题:此处就是unsafe 的来源
// offset of Class.reflectionData instance field
private static final long reflectionDataOffset
= unsafe.objectFieldOffset(Class.class, "reflectionData");
// offset of Class.annotationType instance field
private static final long annotationTypeOffset #### 问题:此处就是offset 的来源
= unsafe.objectFieldOffset(Class.class, "annotationType");
// offset of Class.annotationData instance field
private static final long annotationDataOffset
= unsafe.objectFieldOffset(Class.class, "annotationData");
static <T> boolean casReflectionData(Class<?> clazz,
SoftReference<ReflectionData<T>> oldData,
SoftReference<ReflectionData<T>> newData) {
return unsafe.compareAndSetReference(clazz, reflectionDataOffset, oldData, newData);
}
static boolean casAnnotationType(Class<?> clazz,
AnnotationType oldType,
AnnotationType newType) {
return unsafe.compareAndSetReference(clazz, annotationTypeOffset, oldType, newType);
}
static boolean casAnnotationData(Class<?> clazz,
AnnotationData oldData,
AnnotationData newData) {
return unsafe.compareAndSetReference(clazz, annotationDataOffset, oldData, newData);
}
}
/**
* Reflection support.
*/
// Reflection data caches various derived names and reflective members. Cached
// values may be invalidated when JVM TI RedefineClasses() is called
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Cached names
String simpleName;
String canonicalName;
static final String NULL_SENTINEL = new String();
问题:双亲委派机制
累了,不想写了。参考上面。
自己去点源码:
FileSystemClassLoader loader = new FileSystemClassLoader("src/main/java/");
Class<?> cls2 = loader.loadClass("org.example.jdk17.classLoader.Utils");
System.out.println(cls1.getClassLoader());
cls2.getMethod("printInfo").invoke(null); // 输出: 包名类加载成功!
问题:自定义了一个 String,那么会加载哪个 String?
###忠告不要轻易尝试。建议换个名字。其次看下文
双亲委派模型
确保核心类库的唯一性和安全性,防止恶意替换java.lang下的类。
JVM硬性限制
通过ClassLoader.preDefineClass()方法检查包名,禁止自定义java.*类。
类加载器隔离
不同类加载器加载的同名类(如com.example.String和java.lang.String)被视为完全不同的类,互不干扰。
问题:代码,写了状态机的跳转,
就是定义2个Enum对象。然后在状态表中写明状态的流转过程。然后通过triggerEvent根据状态调用不同状态的策略/工厂处理模式
关键通过UML理清所有的状态转换流程
Map<PaymentState, Map<PaymentEvent, PaymentState>> table = new EnumMap<>(PaymentState.class);
// 标准成功路径
addTransition(table, PaymentState.INITIAL, PaymentEvent.ENQUEUE, PaymentState.IN_QUEUE);
addTransition(table, PaymentState.IN_QUEUE, PaymentEvent.START_PROCESS, PaymentState.WAITING);
addTransition(table, PaymentState.WAITING, PaymentEvent.START_PROCESS, PaymentState.PROCESSING);
addTransition(table, PaymentState.PROCESSING, PaymentEvent.COMPLETE_SEND, PaymentState.SENT);
addTransition(table, PaymentState.SENT, PaymentEvent.RECEIVE_RESULT, PaymentState.RETURNED);
addTransition(table, PaymentState.RETURNED, PaymentEvent.RECONCILE, PaymentState.RECONCILED);
addTransition(table, PaymentState.RECONCILED, PaymentEvent.SETTLE, PaymentState.SETTLED);
addTransition(table, PaymentState.SETTLED, PaymentEvent.COMPLETE, PaymentState.COMPLETED);
问题:future 的原理
###Future 可isDone/isCancel/get/set/cancel
1. Future的核心数据结构与状态机
Future的实现主要基于AQS(AbstractQueuedSynchronizer)框架,以FutureTask这个最常用的Future实现为例,其核心数据结构如下:
Future的原理及JDK实现详解
Future是Java并发编程中用于表示异步计算结果的核心接口,它提供了一种检查计算是否完成、等待计算完成以及获取计算结果的方法。下面我将分步骤详细讲解Future的原理,并在每个步骤后给出JDK中的实例代码。
1. Future的核心数据结构与状态机
Future的实现主要基于AQS(AbstractQueuedSynchronizer)框架,以FutureTask这个最常用的Future实现为例,其核心数据结构如下:
public class FutureTask<V> implements RunnableFuture<V> {
// 任务的状态
private volatile int state;
// 存储计算结果或异常
private Object outcome;
// 执行任务的线程
private volatile Thread runner;
// 等待线程队列的头节点
private volatile WaitNode waiters;
}
状态转换图如下:
NEW → COMPLETING → NORMAL (正常完成)
NEW → COMPLETING → EXCEPTIONAL (异常完成)
NEW → CANCELLED (被取消)
NEW → INTERRUPTING → INTERRUPTED (被中断)
JDK实例代码:创建FutureTask
System.out.println("Is done: " + futureTask.isDone());
System.out.println("Is cancelled: " + futureTask.isCancelled());
2. 任务提交与执行原理
当提交一个任务时,实际发生了以下步骤:
将Callable包装成FutureTask
提交到线程池
执行任务并保存结果
设置执行结果或异常
ExecutorService executor = Executors.newFixedThreadPool(5); // 固定5个线程
Callable<Integer> callable = () -> {
synchronized (lock) { // 同步代码块
System.out.println(Thread.currentThread().getName() +
" - Before update: " + sharedCounter);
sharedCounter++; // 更新共享变量
System.out.println(Thread.currentThread().getName() +
" - After update: " + sharedCounter);
}
return sharedCounter;
};
3. 结果获取机制
当调用get()方法获取结果时,内部实现如下:
获取当前状态
如果任务未完成,进入等待
返回结果或抛出异常
4. 任务完成时的处理机制
当任务执行完成时,会调用set()方法设置结果:
futureTask.setManually("Manually set result");
5. 取消机制的实现
任务取消的实现主要通过CAS操作改变状态实现:
boolean cancelled = future.cancel(true);
检查状态并尝试改变状态
如果允许中断,中断执行线程
问题:布隆过滤器 和zset是什么?
问题:实现不加锁的生产者消费者 我用的 cas
#### 问题:queue 方式
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity = 5; // 缓冲区容量
private final Lock lock = new ReentrantLock();
// 定义两个条件变量:非满条件(生产者用)和非空条件(消费者用)
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
// 生产者方法
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
System.out.println("缓冲区已满,生产者等待...");
notFull.await(); // 释放锁并阻塞,直到notFull被唤醒
}
buffer.offer(item);
System.out.println("生产: " + item + ",当前缓冲区大小: " + buffer.size());
notEmpty.signal(); // 唤醒一个等待notEmpty的消费者
} finally {
lock.unlock(); // 确保锁释放
}
}
#### 问题:CAS 方式
private final AtomicInteger count = new AtomicInteger(0);
#### 问题:BlockedQueue 方式
问题:Java 类加载过程
上面有了,不重复了
问题:各类索引
###哈希索引:极速等值查询的利器
哈希索引是基于哈希表实现的索引结构,它通过哈希函数将键值映射到索引位置来实现快速查找。当用户查询某个特定键值时,哈希索引会先计算该键值的哈希码,然后直接定位到对应的存储位置,理论时间复杂度为O(1),这使得它在等值查询场景下表现极为出色
###B树索引
B树的核心特点是每个节点可以包含多个键和指针(而非常规二叉树的两个),这显著降低了树的高度。一棵m阶的B树满足以下特性:每个节点最多有m个子节点;除根节点外,每个非叶节点至少有⌈m/2⌉个子节点;所有叶子节点都在同一层级上。这种结构使B树在磁盘存储系统中表现优异,因为每次磁盘I/O可以读取一个包含多个键的完整节点。
###B+树
#B+树在B树基础上做了进一步优化:
非叶子节点只存储键值,不存储实际数据,这使得每个节点可以容纳更多键值
所有叶子节点通过指针相连,形成了有序链表,极大提高了范围查询和全表遍历的效率
数据只存储在叶子节点中,查询路径长度一致,性能更稳定
###全文索引:文本搜索的专用引擎
与传统索引不同,全文索引的核心是倒排索引结构,它记录每个单词出现在哪些文档或记录中,以及出现的位置和频率。当执行全文搜索时,数据库可以快速定位包含特定词汇的文档,而无需扫描全部内容。现代全文索引还支持词干提取、同义词扩展和相关性评分等高级功能。
问题:Spark SQL中Sort Merge Join与Sort Join的区别与实现原理
Sort Merge Join(排序合并连接)是Spark SQL中处理大表连接的主要策略,它通过先排序后合并的方式实现高效连接。而Sort Join通常指的是在连接操作前对单表进行排序的预处理步骤,它本身并不是一种独立的连接算法,而是为其他连接策略(如Sort Merge Join)服务的操作。
核心区别在于:
Sort Merge Join是一个完整的连接算法实现,包含shuffle、sort和merge三个阶段
Sort Join只是对数据进行排序的中间步骤,通常作为其他连接策略的预处理阶段
Sort Merge Join的实现原理
Sort Merge Join是Spark SQL默认的大表连接策略,其执行过程分为三个明确的阶段:
Shuffle阶段:
将两张大表根据join key进行重新分区
保证相同join key的记录会被分配到相同的分区
数据分布到整个集群实现并行处理
Sort阶段:
对每个分区节点上的两表数据分别进行排序
排序确保分区内数据按照join key有序排列
使用Spark的sort-based shuffle算法实现高效排序
Merge阶段:
对排好序的两张分区表数据执行连接操作
采用类似归并排序的合并算法:同时遍历两个有序序列
碰到相同join key就合并输出,否则取较小key的一边继续遍历
Sort Join的作用与定位
Sort Join在Spark SQL中通常不是作为独立连接策略存在,而是作为其他连接策略的预处理步骤:
作为Sort Merge Join的组成部分:
在Sort Merge Join中,Sort Join是第二阶段的核心操作
确保分区内数据有序,为后续merge阶段做准备
为其他连接策略优化:
某些情况下,预先排序数据可以优化Shuffle Hash Join的性能
有序数据可以提高局部性,减少随机内存访问
特殊场景的直接应用:
当数据已经按join key排序时,可能跳过部分排序步骤
在自定义连接实现中,可利用预先排序的数据
问题:b+树层数和什么有关
1. 数据总量
数据总量是影响B+树层数的最直接因素:
数据量越大,层数越多:当表中记录数增加时,B+树需要更多层来容纳所有数据
三层B+树的容量:通常3层B+树可以支持百万至数亿条记录。例如,假设每个节点能存储1000个键值,3层B+树可索引1000×1000×1000=10亿条记录
临界点计算:根据计算,当单条数据约1KB时,3层B+树大约可存储2190万条数据(1170×1170×16),这也是为什么建议单表数据量超过2000万时考虑分表的原因
2. 节点容量(度数与页大小)
节点容量决定了每个B+树节点能存储多少键值,直接影响树的"胖瘦":
磁盘页大小:B+树节点通常设计为与磁盘页大小匹配(如InnoDB默认为16KB),以便一次I/O读取整个节点
键值与指针大小:对于bigint主键(8字节)和指针(6字节),16KB页可存储约1170个键值(16×1024/(8+6)≈1170)
字段长度影响:索引字段越长,单个节点能存储的键值越少。例如100字节的字段,8KB页只能存储81个键值(8192/100≈81),会增加层数
3. 索引结构特性
B+树自身的结构特性也会影响层数:
非叶子节点仅存储键值:相比B树,B+树的非叶子节点不存储数据,可以容纳更多键值,从而降低层数
节点填充因子:实际应用中节点不会100%填满,通常保持一定空闲空间(如InnoDB的页填充因子约为15/16),这会影响实际容量计算
唯一性约束:唯一索引可能比非唯一索引需要稍多的层数,因为不允许重复值
问题:b+树叶子结点什么数据
###通过以下UML我们可以看到 BTree 有 root+Node Node中有 +Object[] data // 数据记录(非叶子节点可能为空)
+Node[] children // 子节点指针数组
###而B+数中没有 data 和child, data[] 都放在 LeafNode中,然后通过双向指针提供移动
#### 问题:Btree
@startuml
class BTree {
+int order // B树的阶数
+int height // 树的高度
+Node root // 根节点
}
class Node {
+boolean isLeaf
+int keyCount
+int[] keys // 键值数组
+Object[] data // 数据记录(非叶子节点可能为空)
+Node[] children // 子节点指针数组
}
BTree *-- Node
@enduml
###B+ Tree
@startuml
class BPlusTree {
+int order // B+树的阶数
+int height // 树的高度
+Node root // 根节点
+LeafNode firstLeaf // 首个叶子节点(链表头)
}
abstract class Node {
+boolean isLeaf
+int keyCount
+int[] keys
}
class InternalNode {
+Node[] children // 子节点指针数组
}
class LeafNode {
+Object[] data // 数据记录或主键值
+LeafNode prev // 前驱叶子节点指针
+LeafNode next // 后继叶子节点指针
}
BPlusTree *-- Node
Node <|-- InternalNode
Node <|-- LeafNode
LeafNode --> LeafNode : 双向链表
@enduml
问题:数据库:b 树,b+树,红黑树,聚簇/非聚簇索引,索引覆盖
B树索引
数据库索引结构全面解析:从B树到索引覆盖
数据库索引是数据库系统中用于加速数据检索的核心组件,不同的索引结构适用于不同的场景和需求。本文将深入解析B树、B+树、红黑树等数据结构在数据库索引中的应用,并详细探讨聚簇/非聚簇索引以及索引覆盖的原理与实现。
B树索引:平衡多路搜索的基石
B树(Balanced Tree)是一种自平衡的多路搜索树,由Rudolf Bayer和Edward M. McCreight在1970年发明,专门为磁盘等直接存取存储设备设计。B树在数据库系统中扮演着至关重要的角色,其核心特性使其成为处理大规模数据的理想选择。
B树的核心特性
多路平衡结构:每个节点可以包含多个键值和子节点指针(通常子节点数比键值数多一个),这与二叉树形成鲜明对比。
平衡性保证:所有叶子节点都位于同一层级,确保查询路径长度一致。通过节点分裂(插入时)和合并(删除时)的操作来维持这种平衡。
有序存储:节点内的键值按顺序排列,使得范围查询成为可能。
节点容量控制:除根节点外,每个节点的关键字数至少为t-1,最多为2t-1(t为B树的最小度数)
B+树索引
B+树与B树的关键区别
数据存储位置:
B树:所有节点都可能包含数据
B+树:仅叶子节点存储实际数据或数据指针,内部节点只存储键值和子节点指针
叶子节点连接:
B树:叶子节点互不连接
B+树:所有叶子节点通过指针形成双向链表,极大优化范围查询
节点利用率:
B+树内部节点更紧凑,可存储更多键值,进一步降低树高
红黑树
内存数据库:如Redis的Sorted Set使用跳表与红黑树混合实现
数据库内部结构:某些DBMS用红黑树管理事务锁、内存缓冲区等
索引缓存:热索引节点在内存中可能以红黑树形式组织
红黑树的主要优势在于内存操作效率高,而不足则是树高较大,不适合直接作为磁盘索引结构。在需要严格保证单次操作时间的场景下,红黑树比B+树更有优势。
问题:单链表排序
问题:算法题:二叉查找
问题:线程池七大参数都是什么?
1. corePoolSize(核心线程数)
定义:线程池中保持存活的最小线程数量,即使这些线程处于空闲状态也不会被销毁(除非设置allowCoreThreadTimeOut为true)。
作用:任务提交时,若当前线程数小于corePoolSize,则直接创建新线程执行任务。
2. maximumPoolSize(最大线程数)
定义:线程池允许创建的最大线程数量。当任务队列已满且当前线程数小于maximumPoolSize时,会创建新线程处理任务;若超过此值则触发拒绝策略。
注意:必须大于等于corePoolSize,否则抛出IllegalArgumentException。
3. keepAliveTime(空闲线程存活时间)
定义:当线程数超过corePoolSize时,多余的空闲线程在等待新任务时的最长存活时间。超时后这些线程会被销毁,直到线程数降至corePoolSize。
适用场景:短期高并发场景可设较短时间(如30秒),长期负载可设较长时间(如5分钟)。
4. unit(时间单位)
定义:keepAliveTime的时间单位,支持TimeUnit枚举值(如秒、毫秒等)。
5. workQueue(任务队列)
定义:用于缓存待执行任务的阻塞队列。常见实现类:
ArrayBlockingQueue:有界队列,基于数组,严格FIFO。
LinkedBlockingQueue:无界队列(默认Integer.MAX_VALUE),基于链表,吞吐量高。
SynchronousQueue:不存储任务,直接提交给线程,适用于高响应场景。
PriorityBlockingQueue:支持优先级排序的无界队列。
6. threadFactory(线程工厂)
定义:用于创建新线程的工厂类,可自定义线程名称、优先级、是否为守护线程等属性,便于日志排查。
7. handler(拒绝策略)
定义:当线程池和队列均满时,处理新提交任务的策略。内置策略包括:
AbortPolicy(默认):抛出RejectedExecutionException。
CallerRunsPolicy:由提交任务的线程直接执行任务。
DiscardPolicy:静默丢弃任务。
DiscardOldestPolicy:丢弃队列中最旧的任务,并重新提交当前任务。
问题:6.线程池池化
// 1. 自定义线程池(推荐)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
30, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 2. 提交任务
executor.execute(() -> {
System.out.println("Task executed by " + Thread.currentThread().getName());
});
// 3. 关闭池(优雅终止)
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
问题:JMM
### 功能划分
层次划分:JMM作为抽象规范,隔离Java程序与硬件差异 。
关键协议:内存交互操作和happens-before规则是JMM实现线程安全的基础 。
实践映射:volatile、synchronized等关键字在UML中对应具体的同步机制组件。
### 一致性
缓存一致性:JMM通过内存屏障(如volatile的写屏障)强制CPU缓存刷新到主存,解决硬件层面的可见性问题。
指令重排序:JMM限制编译器和处理器的优化,确保happens-before语义不被破坏。
1. JMM核心抽象模型
@startuml
class JMM {
+定义内存访问规则
+保证跨平台一致性
}
class 主内存 {
+存储共享变量
+volatile修饰的变量直接读写
}
class 工作内存 {
+存储线程私有变量副本
+操作需同步回主内存
}
class 线程 {
+操作工作内存
+通过JMM协议交互
}
class HappensBefore {
+程序顺序规则
+volatile变量规则
+锁规则
+传递性规则
}
JMM --> 主内存 : 管理
JMM --> 工作内存 : 协调
线程 --> 工作内存 : 私有操作
线程 --> 主内存 : 同步数据
HappensBefore --> JMM : 约束执行顺序
@enduml
2. JMM与硬件交互
@startuml
component JMM {
[主内存] as MainMemory
[工作内存] as WorkingMemory
}
component 硬件 {
[CPU缓存] as CPUCache
[内存屏障] as MemoryBarrier
[主存] as RAM
}
JMM.MainMemory --> 硬件.RAM : 物理映射
JMM.WorkingMemory --> 硬件.CPUCache : 缓存副本
JMM --> 硬件.MemoryBarrier : 插入屏障指令\n(禁止重排序)[6,7](@ref)
@enduml
3. 并发工具与JMM的关系
@startuml
class JMM {
+原子性: synchronized/lock
+可见性: volatile
+有序性: happens-before
}
class Volatile变量 {
+写屏障保证可见性
+禁止重排序
}
class Synchronized块 {
+monitorenter/monitorexit
+锁获取释放的happens-before
}
class Atomic类 {
+CAS操作保证原子性
}
JMM --> Volatile变量 : 实现可见性
JMM --> Synchronized块 : 实现原子性/有序性
JMM --> Atomic类 : 替代锁的轻量级原子操作[1,5](@ref)
@enduml
问题:2.jvm内存区域
1. 线程共享区域
堆(Heap):分为新生代(Eden + Survivor)和老年代,存储对象实例和数组,是GC主要区域。
元空间(Metaspace):替代JDK 8之前的永久代,存储类元信息、静态变量等,使用本地内存。
2. 线程私有区域
Java虚拟机栈:每个线程独立,存储栈帧(局部变量表、操作数栈等),方法调用时压栈/出栈。
本地方法栈:服务于JNI调用的本地方法,HotSpot中与虚拟机栈合并。
程序计数器:记录线程执行的字节码指令地址,唯一无OOM的区域。
3. UML
@startuml
class JVMMemory {
+ {static} threadSharedAreas: List<MemoryArea>
+ {static} threadPrivateAreas: List<MemoryArea>
}
'--- 线程共享区域 ---
class Heap {
+ {field} youngGen: YoungGeneration
+ {field} oldGen: OldGeneration
+ allocateObject(): void
+ garbageCollect(): void
}
class YoungGeneration {
+ {field} eden: EdenSpace
+ {field} survivor0: SurvivorSpace
+ {field} survivor1: SurvivorSpace
+ minorGC(): void
}
class OldGeneration {
+ majorGC(): void
}
class Metaspace {
+ {field} classMetadata: ClassInfo[]
+ {field} staticVariables: StaticVar[]
+ unloadClass(): void
}
'--- 线程私有区域 ---
class ThreadPrivateArea {
+ {abstract} thread: Thread
}
class JavaVirtualMachineStack {
+ {field} frames: StackFrame[]
+ pushFrame(): void
+ popFrame(): void
}
class NativeMethodStack {
+ executeNativeMethod(): void
}
class ProgramCounterRegister {
+ {field} instructionAddress: int
}
'--- 关联关系 ---
JVMMemory *-- Heap
JVMMemory *-- Metaspace
JVMMemory *-- ThreadPrivateArea
Heap *-- YoungGeneration
Heap *-- OldGeneration
YoungGeneration *-- EdenSpace
YoungGeneration *-- SurvivorSpace
ThreadPrivateArea <|-- JavaVirtualMachineStack
ThreadPrivateArea <|-- NativeMethodStack
ThreadPrivateArea <|-- ProgramCounterRegister
class StackFrame {
+ {field} localVariables: LocalVarTable
+ {field} operandStack: OperandStack
+ {field} dynamicLink: DynamicLinkage
+ {field} returnAddress: ReturnAddress
}
JavaVirtualMachineStack *-- StackFrame
@enduml
问题:3.垃圾回收算法
一、垃圾回收基础概念
1. 垃圾回收的基本原理
Java垃圾回收机制基于可达性分析原理,从一系列称为"GC Roots"的对象出发,通过引用链追踪所有可达对象,未被引用的对象则被视为垃圾。GC Roots包括:
虚拟机栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
活动线程
2. 对象存活判断算法
引用计数法
每个对象有一个引用计数器
引用增加时计数器+1,减少时-1
计数器为0时对象可被回收
缺点:无法解决循环引用问题
可达性分析算法
从GC Roots开始遍历对象引用图
标记所有可达对象
未标记对象视为垃圾
现代JVM主要采用此算法
二、主要垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
工作原理:
标记阶段:从GC Roots出发标记所有可达对象
清除阶段:回收未被标记的对象内存
优点:
实现简单直接
适用于基本场景
缺点:
产生内存碎片,可能导致大对象分配失败
标记和清除过程需要暂停应用线程(STW)
2. 复制算法(Copying)
工作原理:
将内存分为大小相等的两块
只使用其中一块,当该块满时
将存活对象复制到另一块
清空已使用块
优点:
简单高效,无内存碎片
对象分配速度快(指针碰撞)
缺点:
内存利用率仅50%
存活率高时复制效率低
应用场景:
主要用于新生代回收
3. 标记-整理算法(Mark-Compact)
工作原理:
标记阶段:同标记-清除
整理阶段:将存活对象向内存一端移动
清除阶段:清理边界外的空间
优点:
无内存碎片问题
内存利用率高
缺点:
移动对象开销大
需要全程STW
应用场景:
主要用于老年代回收
4. 分代收集算法(Generational Collection)
核心思想:
### 根据对象生命周期将堆分为新生代和老年代
对不同代采用不同回收算法
新生代特点:
对象生命周期短,回收频繁
采用复制算法
分为Eden区和两个Survivor区(8:1:1)
老年代特点:
对象生命周期长,回收不频繁
采用标记-清除或标记-整理算法
工作流程:
新对象分配在Eden区
Eden满时触发Minor GC,存活对象复制到Survivor区
对象在Survivor区间复制,年龄增加
年龄达到阈值(默认15)晋升老年代
老年代空间不足时触发Full GC
5. 分区算法(Region-Based Collection)
### 代表:G1收集器采用此算法
工作原理:
将堆划分为多个大小相等的Region
优先回收垃圾最多的Region
可预测停顿时间
优点:
更灵活的内存管理
避免全堆扫描
适合大内存应用
缺点:
实现复杂
调优成本较高
问题:4.Java 异常体系
Throwable
├── Error(不可恢复的严重错误)
└── Exception(可处理的异常)
├── RuntimeException(运行时异常/非检查异常)
└── 非 RuntimeException(检查异常/编译时异常)
Throwable:所有异常和错误的基类,提供异常信息(如 getMessage())和堆栈跟踪(printStackTrace())。
Error:表示 JVM 或系统级别的严重错误(如内存溢出、栈溢出),程序通常无法处理,只能终止运行。
示 例:OutOfMemoryError、StackOverflowError。
Exception:程序可捕获和处理的异常,分为两类:
检查异常(Checked Exception):编译器强制要求处理(如 IOException、SQLException),否则编译不通过。
非检查异常(Unchecked Exception):继承自 RuntimeException,编译器不强制处理(如 NullPointerException、ArrayIndexOutOfBoundsException)。
问题:5.有没有自定义过异常
public class BusinessException extends RuntimeException {
private String errorCode; // 业务错误码
private String errorMessage;
public BusinessException(String errorCode, String errorMessage) {
super(errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
// Getter方法
}
throw new BusinessException("000001", "业务异常");
问题:手写代码题(如CAS实现、排序优化)
问题:手写一下自旋锁的类
自旋锁(Spinlock)是一种基于忙等待(Busy-Waiting)的同步机制,主要用于多线程/多核环境中保护共享资源。根据不同的实现方式和特性,自旋锁可以分为以下几种类型:
1. 普通自旋锁(Basic Spinlock)
特点:最简单的自旋锁实现,仅通过原子操作(如CAS)控制锁状态。
适用场景:适用于锁竞争不激烈、临界区执行时间极短的场景。
自旋锁(Spinlock)是一种基于忙等待(Busy-Waiting)的同步机制,主要用于多线程/多核环境中保护共享资源。根据不同的实现方式和特性,自旋锁可以分为以下几种类型:
2. 公平自旋锁(Fair Spinlock)
(1) Ticket Lock(票据锁)
特点:采用“取号-叫号”机制,保证线程按请求顺序获取锁。
实现方式:
维护两个变量:ticketNum(取号)和serviceNum(叫号)。
线程获取锁时先取号,自旋等待直到serviceNum等于自己的号。
3. CLH锁(Craig, Landin, Hagersten Lock)
特点:
基于隐式链表,每个线程在前驱节点的状态上自旋。
减少缓存一致性流量,适合SMP架构。
优点:
公平性较好,减少全局变量竞争。
每个线程自旋在本地变量上,降低缓存同步开销
问题:什么是死锁?死锁的产生条件是什么?解决死锁的基本方法有哪
什么是死锁?
死锁(Deadlock)是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
哲学家进餐问题:五位哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五只碗和五根筷子。哲学家必须拿到两根筷子才能进餐,只有进餐结束之后才会放下已有的筷子。一种典型的死锁情况就是哲学家们每人都拿到了一根筷子,这样他们就一直等待别人放下筷子但是又不放下自己的筷子,这样就陷入僵持状态,谁也无法进餐。
### 死锁的典型示例
资源竞争示例:在一个计算机系统中,仅有一台打印机A和一个输入设备B时,进程P1正在占用输入设备B,并且要求使用打印机A,但是打印机A此时被进程P2占据,P2又要求利用P1所占据的输入设备B。两个进程都在不停地等待,不能继续进行,这时两个进程就会陷入死锁状态。
哲学家进餐问题:五位哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五只碗和五根筷子。哲学家必须拿到两根筷子才能进餐,只有进餐结束之后才会放下已有的筷子。一种典型的死锁情况就是哲学家们每人都拿到了一根筷子,这样他们就一直等待别人放下筷子但是又不放下自己的筷子,这样就陷入僵持状态,谁也无法进餐。
死锁的产生条件
死锁的概念、产生条件与解决方法
死锁是计算机科学和操作系统领域中一个重要的概念,特别是在多线程编程和并发系统中经常遇到的问题。下面我将从死锁的定义、产生条件以及解决方法三个方面进行全面阐述。
一、什么是死锁?
死锁(Deadlock)是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
更专业的定义是:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。死锁会导致占用公共资源不释放,甚至可能会造成饥饿问题。
死锁的典型示例
资源竞争示例:在一个计算机系统中,仅有一台打印机A和一个输入设备B时,进程P1正在占用输入设备B,并且要求使用打印机A,但是打印机A此时被进程P2占据,P2又要求利用P1所占据的输入设备B。两个进程都在不停地等待,不能继续进行,这时两个进程就会陷入死锁状态。
哲学家进餐问题:五位哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五只碗和五根筷子。哲学家必须拿到两根筷子才能进餐,只有进餐结束之后才会放下已有的筷子。一种典型的死锁情况就是哲学家们每人都拿到了一根筷子,这样他们就一直等待别人放下筷子但是又不放下自己的筷子,这样就陷入僵持状态,谁也无法进餐。
二、死锁的产生条件
死锁的发生必须同时满足以下四个必要条件,这四个条件缺一不可:
互斥条件(Mutual Exclusion):指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。简单说就是一个资源每次只能被一个执行流使用。
请求与保持条件(Hold and Wait):指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。即一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件(No Preemption):指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。即一个执行流已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件(Circular Wait):指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。即若干执行流之间形成一种头尾相接的循环等待资源的关系。
解决死锁的基本方法
统一锁顺序:通过强制所有线程按固定顺序获取锁来消除循环等待链。例如按对象哈希值排序获取锁,确保所有线程遵循相同顺序。
锁超时+重试:结合ReentrantLock.tryLock()实现超时机制,破坏"持有并等待"条件。设置分层超时时间,失败时释放所有锁并重试,避免永久阻塞。
锁管理器:通过中央调度管理锁申请,彻底消除循环等待可能。需要确保原子性获取多个锁时,通过全局管理器检查锁状态。
银行家算法实现:通过资源分配算法动态避免进入不安全状态。线程声明最大资源需求,系统模拟分配,仅当存在安全序列时才分配
如何防止死锁
1. 如何能发现java 程序中存在死锁
启动 DeadlockExample.java
https://gitee.com/snetlogon20/jdk17Test/blob/master/src/main/java/org/example/jdk17/Threads/lock/deadLock/DeadlockExample.java
jps
jstack -l 15724 > thread_dump.txt
### thread_dump.txt文件中能查到deadlock字样
"Thread-1" #17 prio=5 os_prio=0 cpu=0.00ms elapsed=114.84s tid=0x000002181157c950 nid=0x5c0 waiting for monitor entry [0x0000007aa3eff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.example.jdk17.Threads.lock.deadLock.DeadlockExample.lambda$main$1(DeadlockExample.java:27)
- waiting to lock <0x0000000698a0d2e8> (a java.lang.Object)
- locked <0x0000000698a0d2f8> (a java.lang.Object)
at org.example.jdk17.Threads.lock.deadLock.DeadlockExample$$Lambda$15/0x0000021813001418.run(Unknown Source)
at java.lang.Thread.run(java.base@17.0.12/Thread.java:842)
2. 使用 try lock, 探测锁,当互斥锁都没锁时,进入处理程序
https://gitee.com/snetlogon20/jdk17Test/blob/master/src/main/java/org/example/jdk17/Threads/lock/deadLock/TryLockExample.java
3. 设置超时锁,超时退出
4. 统一锁顺序避免死锁
Java中检测和解除死锁的实践指南
在Java并发编程中,死锁是一个常见且棘手的问题。当多个线程互相等待对方释放锁时,程序就会陷入死锁状态,导致系统部分或全部功能停滞。下面我将详细介绍如何检测死锁状态以及如何通过代码示例来解决死锁问题。
一、死锁检测方法
1. 使用JDK工具检测死锁
Java提供了多种工具来检测死锁状态:
jstack工具:
jstack -l <pid> > thread_dump.txt
然后搜索输出中的"deadlock"关键词,jstack会自动标记死锁线程
JConsole/VisualVM:
这些图形化工具可以直接显示死锁线程和锁的依赖关系
编程方式检测:
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = mxBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("死锁线程: " + threadInfo.getThreadName());
System.out.println("等待锁: " + threadInfo.getLockName());
System.out.println("被线程持有: " + threadInfo.getLockOwnerName());
}
}
二、死锁解决示例代码
1. 基本死锁示例
首先我们来看一个典型的死锁场景:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock 2!");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock 1!");
}
}
});
thread1.start();
thread2.start();
}
}
这个程序会陷入死锁,因为两个线程以相反的顺序获取锁。
2. 使用tryLock避免死锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
boolean acquiredLock1 = false;
boolean acquiredLock2 = false;
try {
acquiredLock1 = lock1.tryLock();
acquiredLock2 = lock2.tryLock();
if (acquiredLock1 && acquiredLock2) {
// 成功获取两个锁,执行业务逻辑
System.out.println("执行method1的业务逻辑");
} else {
// 未能获取全部锁,执行备用逻辑
System.out.println("未能获取全部锁,执行备用逻辑");
}
} finally {
if (acquiredLock1) lock1.unlock();
if (acquiredLock2) lock2.unlock();
}
}
public void method2() {
// 类似的实现,顺序可以不同
}
}
3. 带超时的锁获取
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public boolean tryDoSomething(long timeout, TimeUnit unit)
throws InterruptedException {
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
// 成功获取两个锁
doSomething();
return true;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
if (System.nanoTime() > stopTime) {
return false; // 超时未获取锁
}
// 随机休眠避免活锁
TimeUnit.NANOSECONDS.sleep(100);
}
}
private void doSomething() {
// 业务逻辑
}
}
4. 统一锁顺序避免死锁
public class OrderedLockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
}
public void method2() {
synchronized (lock1) { // 与method1相同的顺序
synchronized (lock2) {
// 业务逻辑
}
}
}
}
三、高级死锁处理技术
1. 死锁检测与恢复
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DeadlockDetector {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
private final ThreadMXBean mxBean =
ManagementFactory.getThreadMXBean();
private final long checkInterval;
private final TimeUnit unit;
private final Runnable deadlockHandler;
public DeadlockDetector(long checkInterval, TimeUnit unit,
Runnable deadlockHandler) {
this.checkInterval = checkInterval;
this.unit = unit;
this.deadlockHandler = deadlockHandler;
}
public void start() {
scheduler.scheduleAtFixedRate(() -> {
long[] deadlockedThreads = mxBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
deadlockHandler.run();
}
}, 0, checkInterval, unit);
}
}
2. 使用JMX监控死锁
import javax.management.MBeanServer;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockMonitor {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
while (true) {
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos =
threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("死锁检测到线程: " + threadInfo.getThreadName());
System.out.println("等待锁: " + threadInfo.getLockName());
System.out.println("被线程持有: " + threadInfo.getLockOwnerName());
System.out.println("堆栈跟踪:");
for (StackTraceElement element : threadInfo.getStackTrace()) {
System.out.println("\t" + element);
}
}
// 这里可以添加死锁恢复逻辑
// 比如中断某个线程或采取其他措施
}
try {
Thread.sleep(5000); // 每5秒检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
四、实际应用建议
### 使用Blocking queue ,在保证线程安全的情况下尽量把锁的逻辑下推。
预防优于治疗:在设计阶段就考虑锁的顺序和获取策略
缩小锁范围:只锁定必要的代码块,减少持有锁的时间
使用高级并发工具:考虑使用java.util.concurrent包中的高级工具如ConcurrentHashMap、Semaphore等
监控生产环境:在生产环境中部署死锁检测机制
代码审查:定期审查多线程代码,特别是锁的使用部分
问题:算法题一:a+b+c=0
问题:17.算法题二:链表倒数第 N 个
问题:线上问题怎么排查? Full GC 频繁怎么办
一、Full GC频繁的常见原因
在开始排查之前,我们需要了解哪些情况会导致Full GC频繁发生:
内存泄漏:对象无法被回收,导致老年代内存逐渐被占满
大对象直接进入老年代:如未分页的大数组、大集合等
对象过早晋升到老年代:由于新生代空间不足或YGC频繁,对象在达到年龄阈值前就被晋升
元空间(Metaspace)不足:类加载过多或动态生成类过多
System.gc()显式调用:代码中直接调用了System.gc()
JVM参数配置不合理:堆大小、新生代/老年代比例、Survivor区比例等设置不当
CMS并发模式失败:CMS垃圾回收器在并发清理阶段失败
二、Full GC排查工具与方法
1. 启用并分析GC日志
GC日志是排查Full GC问题最重要的信息来源,需要在JVM启动参数中添加以下参数启用详细GC日志记录:
# JDK 8及之前版本
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
# JDK 9+版本
-Xlog:gc*:file=/path/to/gc.log:time:filecount=5,filesize=10M
分析GC日志时,需要关注以下关键信息:
Full GC发生的频率和时间间隔
Full GC的触发原因(Allocation Failure、Metadata GC Threshold等)
每次Full GC前后各内存区域的使用量变化
Full GC的耗时
老年代使用率是否持续增长
2. 使用JVM监控工具
jstat命令
jstat -gcutil <pid> 1000 # 每秒输出一次GC统计信息
jstat输出中各列含义:
S0/S1: Survivor区使用百分比
E: Eden区使用百分比
O: 老年代使用百分比
M: 元空间使用百分比
YGC/YGCT: Young GC次数和总耗时
FGC/FGCT: Full GC次数和总耗时
jmap命令
# 查看堆内存中的对象分布
jmap -histo <pid> | head -n20
# 生成堆转储文件
jmap -dump:format=b,file=heapdump.hprof <pid>
3. 使用可视化分析工具
MAT(Memory Analyzer Tool):分析堆转储文件,查找内存泄漏和大对象
GCViewer:可视化分析GC日志,查看GC频率和内存变化趋势
JVisualVM:实时监控JVM内存使用情况
GCEasy:在线GC日志分析工具,提供自动化的分析报告
三、Full GC排查步骤
1. 确认问题现象
通过监控工具确认Full GC的频率、持续时间及对服务的影响:
Full GC发生的频率(如每分钟几次)
每次Full GC的停顿时间
服务响应时间是否与Full GC发生时间吻合
系统吞吐量是否下降
如何确定或者观察到频繁full GC?
1. 运行以下程序模拟
https://gitee.com/snetlogon20/jdk17Test/blob/master/src/main/java/org/example/jdk17/gc/FullGCTest.java
VM 参数 -Xms100m -Xmx100m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+UseSerialGC -Xlog:gc*:file=D:\workspace_java\jdk17Test\jdk17Test\src\main\java\org\example\jdk17\gc\gc.log:time:filecount=5,filesize=10M
2. 使用以下命令查看, 发现频繁full gc
jconsole
Jps
jstat -gcutil 13672 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 22.81 84.96 19.83 95.31 79.73 53 0.051 0 0.000 - - 0.051
23.14 0.00 2.08 19.83 95.40 79.73 54 0.053 0 0.000 - - 0.053
23.14 0.00 15.80 19.83 95.40 79.73 54 0.053 0 0.000 - - 0.053
0.00 23.46 9.95 19.83 95.41 79.73 55 0.055 0 0.000 - - 0.055
0.00 23.46 26.46 19.83 95.41 79.73 55 0.055 0 0.000 - - 0.055
23.79 0.00 6.02 19.83 95.46 79.73 56 0.057 0 0.000 - - 0.057
23.79 0.00 19.79 19.83 95.46 79.73 56 0.057 0 0.000 - - 0.057
0.00 24.02 16.67 19.83 95.51 79.73 57 0.060 0 0.000 - - 0.060
0.00 24.02 32.41 19.83 95.51 79.73 57 0.060 0 0.000 - - 0.060
8.38 0.00 9.96 20.62 95.64 79.73 58 0.063 0 0.000 - - 0.063
8.38 0.00 25.69 20.62 95.64 79.73 58 0.063 0 0.000 - - 0.063
问题:SpringBoot 的优缺点分析
一、Spring Boot的核心优点
简化配置与快速开发
自动配置:根据类路径依赖自动配置Spring应用,减少样板代码(如数据源、Web服务器等)。
起步依赖(Starter):通过spring-boot-starter-*快速集成功能模块(如Web、JPA、Security等),简化依赖管理。
内嵌服务器:内置Tomcat、Jetty等容器,无需独立部署WAR文件,支持一键运行。
生产就绪特性
监控与管理:通过Actuator模块提供健康检查、指标收集、日志管理等生产级功能。
外部化配置:支持application.properties/yml多环境配置,灵活切换开发、测试、生产环境。
微服务与分布式支持
与Spring Cloud无缝集成,轻松实现服务发现、负载均衡、配置中心等微服务架构需求。
支持Docker容器化部署,适配云原生场景。
强大的生态系统与社区
背靠Spring生态,集成Spring Data、Spring Security等组件。
活跃的社区和丰富的文档资源,问题解决效率高。
二、Spring Boot的主要缺点
学习曲线与复杂性
自动配置机制对初学者不透明,需理解底层原理(如@Conditional注解)才能深度定制。
依赖管理可能引入冲突,尤其是第三方库版本兼容性问题。
性能开销
内存占用:默认配置下(如内嵌Tomcat、自动加载Bean)内存消耗较高,小型应用可能资源浪费。
启动时间:大型项目因类扫描和Bean初始化可能导致启动较慢。
灵活性受限
约定优于配置原则可能限制个性化需求,需手动覆盖自动配置(如自定义DataSource)。
过度封装导致调试困难,如自动配置冲突时排查复杂。
版本迭代频繁
Spring Boot更新快,版本间可能存在不兼容变更,升级需谨慎测试。
三、适用场景与优化建议
推荐场景:快速构建微服务、企业级应用、原型开发;适合中小型项目或团队标准化开发。
优化方向:
性能调优:启用懒加载(@Lazy)、调整JVM参数(如G1 GC)、减少自动装配。
内存管理:监控堆外内存使用,避免静态集合导致泄漏。
依赖精简:通过spring-boot-starter-*按需引入依赖,排除无用模块。
### CVM issue, 冲突排查 都是问题。 尽量核心做小包,加载到框架中。这样升级起来就方便。
问题:9.SpringBoot 的启动源码分析
一、启动入口与初始化阶段
入口方法
Spring Boot应用通过main方法启动,调用SpringApplication.run(启动类.class, args),实际执行以下两步:
// 1. 构造SpringApplication实例
SpringApplication app = new SpringApplication(primarySources);
// 2. 执行run方法启动
return app.run(args);
初始化操作:在构造函数中完成应用类型推断、初始化器(ApplicationContextInitializer)和监听器(ApplicationListener)加载。
应用类型推断
通过WebApplicationType.deduceFromClasspath()检测类路径,判断应用类型:
SERVLET:存在DispatcherServlet(传统Web应用)。
REACTIVE:存在DispatcherHandler且无Servlet相关类(响应式Web应用)。
NONE:非Web应用。
加载扩展组件
从META-INF/spring.factories加载ApplicationContextInitializer和ApplicationListener实现类,通过反射实例化。这是Spring Boot自动配置的基础机制。
二、环境准备与上下文创建
环境配置
prepareEnvironment()方法完成:
加载application.properties/yml配置文件。
处理命令行参数(--开头的参数)。
发布ApplicationEnvironmentPreparedEvent事件,触发监听器处理环境变量。
创建应用上下文
根据应用类型实例化不同的ApplicationContext:
SERVLET:AnnotationConfigServletWebServerApplicationContext(内嵌Tomcat/Jetty)。
REACTIVE:AnnotationConfigReactiveWebServerApplicationContext。
NONE:AnnotationConfigApplicationContext(普通Java应用)。
三、上下文刷新与容器启动
刷新上下文(核心)
调用refreshContext(context),最终执行AbstractApplicationContext.refresh(),完成:
Bean定义加载:扫描@Component、@Bean等注解。
依赖注入:解析@Autowired等注解。
内嵌Web服务器启动:对于Web应用,在onRefresh()阶段初始化Tomcat/Jetty实例。
后置处理
执行CommandLineRunner和ApplicationRunner接口的实现类(按@Order顺序)。
发布ContextRefreshedEvent事件,标志应用启动完成。
四、关键扩展点与设计思想
扩展机制
初始化器:通过ApplicationContextInitializer在上下文准备阶段自定义逻辑(如修改环境变量)。
监听器:通过ApplicationListener响应启动事件(如环境准备完成、上下文刷新完成)。
约定优于配置
自动配置基于条件注解(如@ConditionalOnClass),根据类路径动态启用功能模块。
内嵌服务器通过WebServerFactory接口抽象,支持多种容器(Tomcat/Jetty/Undertow)。
UML
@startuml SpringBoot启动时序图
participant User
participant MainClass
participant SpringApplication
participant Environment
participant ApplicationContext
participant BeanFactory
participant AutoConfiguration
participant EmbeddedServer
participant Runner
User -> MainClass : main()
MainClass -> SpringApplication : run(主类, args)
group 初始化阶段
SpringApplication -> SpringApplication : 推断应用类型(Servlet/Reactive)
SpringApplication -> SpringApplication : 加载META-INF/spring.factories中的\nApplicationContextInitializer和ApplicationListener[1,5](@ref)
end
SpringApplication -> Environment : prepareEnvironment()
note left: 配置加载优先级:\n1.命令行参数\n2.application-{profile}.yml\n3.application.yml[3,7](@ref)
SpringApplication -> ApplicationContext : createApplicationContext()
note right: 根据类型创建上下文\n(如AnnotationConfigServletWebServerApplicationContext)[1,4](@ref)
ApplicationContext -> BeanFactory : obtainFreshBeanFactory()
BeanFactory -> BeanFactory : 扫描@Component/@Bean注册BeanDefinition[1,8](@ref)
group 自动配置阶段
ApplicationContext -> AutoConfiguration : invokeBeanFactoryPostProcessors()
note left: 关键步骤:\n1.解析@EnableAutoConfiguration\n2.过滤条件装配类\n(如@ConditionalOnClass)[2,5](@ref)
end
ApplicationContext -> BeanFactory : registerBeanPostProcessors()
note right: 注册处理器如:\n1.AutowiredAnnotationBeanPostProcessor\n2.CommonAnnotationBeanPostProcessor[1,7](@ref)
ApplicationContext -> BeanFactory : finishBeanFactoryInitialization()
group Bean生命周期
BeanFactory -> BeanFactory : 实例化单例Bean
BeanFactory -> BeanFactory : 解决循环依赖(三级缓存)[1,7](@ref)
BeanFactory -> BeanFactory : 执行@PostConstruct和AOP代理[2,5](@ref)
end
ApplicationContext -> EmbeddedServer : onRefresh()
note left: 启动Tomcat/Jetty等容器\n(若检测到Servlet环境)[4,7](@ref)
ApplicationContext -> Runner : 执行ApplicationRunner/\nCommandLineRunner[6,7](@ref)
ApplicationContext -> ApplicationContext : finishRefresh()
note right: 发布事件:\n1.ContextRefreshedEvent\n2.ApplicationReadyEvent[1,3](@ref)
SpringApplication --> MainClass : 返回运行上下文
@enduml
问题:3.CAP 理论的理解
问题:孤儿进程;僵尸进程,怎么预防,怎么解决。
1. 孤儿进程(Orphan Process)
定义:孤儿进程是指父进程已经终止或退出,但其子进程仍在运行的进程。这种情况下,子进程成为"孤儿",被系统的init进程(进程ID为1)接管。
产生原因:
父进程因系统错误或bug意外退出
父进程设计上的缺陷,没有正确处理子进程的终止
父进程被系统管理员或程序操作人为终止
在shell脚本或批处理程序中,子进程在父进程结束后仍在运行
系统处理机制:
孤儿进程的父进程ID(PPID)会被设置为1(init进程)
init进程会定期调用wait()清理孤儿进程
这种机制防止了孤儿进程变成僵尸进程
2. 僵尸进程(Zombie Process)
定义:僵尸进程是指子进程已经终止,但父进程尚未调用wait()或waitpid()系统调用来获取子进程的终止状态,导致子进程的进程描述符仍然存在的进程。
产生条件:
子进程已终止
父进程未调用wait()/waitpid()
父进程仍然存在
系统表现:
进程状态显示为"Z"
不再占用CPU和内存资源,但保留进程ID和退出状态信息
在进程表中占有一个条目
四、解决方法
1. 孤儿进程的解决
通常不需要特别处理,因为:
init进程会自动接管并清理孤儿进程
系统有完善的机制处理它们
但在特殊情况下可以:
手动终止:通过kill命令终止孤儿进程
在分布式系统中:实现孤儿进程检测和清理机制
2. 僵尸进程的解决
终止父进程:
杀死僵尸进程的父进程(僵尸进程的父进程必然存在)
僵尸进程成为"孤儿进程",过继给1号进程init,init会负责清理
命令:kill -9 父进程PID
五、 解决关键代码
1. 增加 wait for
Process process = pb.start();
int exitCode = process.waitFor();
2. 增加 ProcessHandle
ProcessHandle handle = process.toHandle();
process.onExit().thenAccept(p -> {
System.out.println("退出码: " + p.exitValue()); // 直接使用Process的exitValue()
});
3. 增加 Monitor
Thread monitorThread = new Thread(() -> {
try {
int exitCode = process.waitFor();
System.out.println("子进程已结束,退出码: " + exitCode);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
monitorThread.setDaemon(true);
monitorThread.start();
4. 增加 shutdownWithHook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("JVM关闭中,终止子进程...");
process.destroy();
}));
yield 的概念和作用
yield()方法的基本概念
1. 方法定义与作用
yield()是Thread类的一个静态native方法,其声明为:
public static native void yield();
它的核心作用是提示线程调度器当前线程愿意放弃当前使用的CPU资源,使线程从运行状态(Running)返回到就绪状态(Runnable),从而让其他具有相同或更高优先级的线程有机会运行。
yield()的调度行为具有以下特点:
非强制性:只是向调度器发出提示,调度器可以忽略此提示
不保证线程切换:调用yield()后,当前线程可能立即重新获得CPU
优先级影响:理论上会优先让给相同或更高优先级的线程,但实际行为取决于具体实现
不释放锁:与sleep()类似,yield()不会释放线程持有的任何锁资源
在需要多个线程协作完成任务的场景中,yield()可以让其他线程有机会运行,提高整体效率。例如:
### 总结
1. Threads 中的 yield 就是让出 CPU 切片,分给其他进程。
2. Switch 中的yield 纯粹就是返回.
问题:Synchronized 修饰class/function/变量
// 1. 实例方法级别同步(锁当前对象实例)
public synchronized void incrementInstance() {
instanceCount++; // 线程安全的实例变量操作[1,5](@ref)
}
// 2. 静态方法级别同步(锁整个类)
public static synchronized void incrementStatic() {
staticCount++; // 线程安全的静态变量操作[1,4](@ref)
}
// 3. 同步代码块(锁自定义对象)
public void incrementWithCustomLock() {
synchronized (lock) { // 使用私有锁对象,避免外部干扰[2,6](@ref)
instanceCount++;
}
}
// 4. 类级别同步代码块(锁Class对象)
public void incrementWithClassLock() {
synchronized (SynchronizedDemo.class) { // 显式锁定类对象[2,4](@ref)
staticCount++;
}
}
然后问了 http 请求 request 内容具体放在什么位置
HTTP请求的完整结构
HTTP请求由三部分组成,按顺序排列为:
1. 请求行(Request Line)
位置:请求的第一行
内容:包含请求方法(如GET、POST)、请求目标(URI)和协议版本(如HTTP/1.1)
示例:
GET /api/data?id=1 HTTP/1.1
2. 请求头(Request Headers)
位置:请求行之后,以一个空行(\r\n)结束
内容:键值对形式的元数据,描述客户端环境、内容类型、缓存策略等
示例:
Host: example.com
User-Agent: Mozilla/5.0
Accept: application/json
Content-Type: application/x-www-form-urlencoded
3. 请求体(Request Body)
位置:空行之后(仅某些请求方法如POST、PUT包含)
内容:客户端提交的数据(如表单、JSON、文件等)
示例:
username=admin&password=123456
或
{"key": "value"}
session 和 cookie
Session 和 Cookie 是 Web 开发中用于跟踪用户状态的两种核心机制,它们在存储位置、安全性、生命周期等方面存在显著差异,但常配合使用以实现完整的会话管理。以下是详细对比和解释:
Cookie
定义:由服务器生成并发送到客户端(浏览器)的小型文本数据,存储在用户本地,用于记录用户身份或偏好。
存储位置:客户端(浏览器内存或硬盘)。
安全性:较低,可能被篡改或窃取(如通过 XSS 攻击),需通过 Secure(仅 HTTPS 传输)和 HttpOnly(禁止 JavaScript 访问)属性增强防护。
生命周期:可设为会话级(关闭浏览器失效)或持久级(通过 Expires/Max-Age 设置过期时间)。
Session
定义:服务器端存储的用户会话数据,通过唯一的 Session ID 关联客户端请求。
存储位置:服务器内存、文件、数据库(如 Redis)。
安全性:较高,敏感数据不暴露给客户端,但需防范 Session ID 劫持(如通过 HTTPS 和定期更换 ID)。
生命周期:通常随会话结束(如浏览器关闭)或超时失效,也可手动设置过期时间。
2. 工作原理
Cookie 流程
首次请求:服务器通过响应头 Set-Cookie 发送 Cookie 到浏览器。
存储:浏览器保存 Cookie 至本地(内存或文件)。
后续请求:浏览器自动在请求头 Cookie 中附带该数据,供服务器识别用户。
Session 流程
创建会话:用户首次访问时,服务器生成唯一 Session ID 并存储对应数据。
传递 ID:通常通过 Cookie(如 JSESSIONID)或 URL 重写发送给客户端。
后续请求:客户端携带 Session ID,服务器据此查找会话数据。
Java 线程池原理介绍一下
Java线程池原理深度解析
线程池是Java并发编程中的核心组件,它通过池化技术实现了线程的高效管理和复用,显著提升了系统性能和资源利用率。下面我将从线程池的设计思想、核心原理、工作流程、关键参数及实际应用等多个维度进行全面解析。
一、线程池的核心概念与优势
线程池是一种池化技术,其核心思想是预先创建一定数量的线程并保存在池中,当有任务提交时,从池中获取线程执行任务,任务完成后线程返回池中等待下次任务,而非直接销毁。
主要优势包括:
降低资源消耗:通过复用已创建的线程,减少线程频繁创建和销毁的性能开销
提高响应速度:任务到达时可直接使用现有线程执行,无需等待线程创建
提高线程可管理性:统一管理线程生命周期,避免无限制创建线程导致的系统资源耗尽
提供任务队列和拒绝策略:缓冲突增任务,保证系统稳定运行
二、线程池的核心实现原理
1. 核心类与接口
Java线程池基于java.util.concurrent包实现,核心类包括:
Executor:基础执行接口,定义execute()方法
ExecutorService:扩展了Executor,提供更丰富的管理方法如submit()、shutdown()等
ThreadPoolExecutor:线程池的核心实现类,提供完整的线程池功能
Executors:工具类,提供创建预配置线程池的工厂方法
2. 线程池的七大核心参数
ThreadPoolExecutor的构造函数包含七个关键参数,共同决定了线程池的行为特性:
参数 说明 影响
corePoolSize 核心线程数 线程池长期维持的线程数量,即使空闲也不会回收(除非设置allowCoreThreadTimeOut)
maximumPoolSize 最大线程数 线程池允许创建的最大线程数量
keepAliveTime 线程空闲时间 非核心线程空闲超过此时间将被回收
unit 时间单位 空闲时间的单位(秒、毫秒等)
workQueue 任务队列 用于保存待执行任务的阻塞队列
threadFactory 线程工厂 用于创建新线程,可定制线程名称、优先级等
handler 拒绝策略 当线程池和队列都满时的处理策略
3. 线程池的工作流程
线程池处理任务的核心流程如下:
任务提交:通过execute()或submit()方法提交任务
核心线程分配:
若当前线程数 < corePoolSize,立即创建新线程执行任务
否则尝试将任务放入工作队列
队列处理:
若队列未满,任务入队等待执行
若队列已满且线程数 < maximumPoolSize,创建新线程执行任务
拒绝策略:
若队列已满且线程数达到maximumPoolSize,执行拒绝策略
线程复用机制:线程执行完任务后,会通过getTask()方法从队列获取新任务,实现线程的循环利用。
三、线程池的任务队列与拒绝策略
1. 常见任务队列类型
线程池的工作队列决定了任务的缓冲策略,常见实现包括:
### 工作队列的选取
ArrayBlockingQueue:基于数组的有界队列,FIFO原则
LinkedBlockingQueue:基于链表的队列,默认无界(可设置容量)
SynchronousQueue:不存储元素的直接交接队列,每个插入操作必须等待移除操作
PriorityBlockingQueue:具有优先级的无界队列
2. 内置拒绝策略
当线程池和队列都饱和时,会触发拒绝策略,JDK提供了四种内置策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常
CallerRunsPolicy:由提交任务的线程自己执行该任务
DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交当前任务
DiscardPolicy:静默丢弃无法处理的任务
开发者也可以实现RejectedExecutionHandler接口自定义拒绝策略。
四、线程池的生命周期与状态管理
线程池内部通过原子变量ctl维护运行状态和线程数量,包含五种状态:
RUNNING:正常运行状态,接受新任务并处理队列任务
SHUTDOWN:调用shutdown()后进入,不再接受新任务但会处理队列中的任务
STOP:调用shutdownNow()后进入,不再接受新任务也不处理队列任务,并中断正在执行的任务
TIDYING:过渡状态,所有任务已终止,workerCount为0,将执行terminated()钩子方法
TERMINATED:terminated()方法执行完成后进入最终状态
关闭方法对比:
shutdown():平缓关闭,等待已提交任务完成
shutdownNow():立即关闭,尝试中断所有任务并返回未执行任务列表
五、线程池的最佳实践
1. 线程池参数配置建议
CPU密集型任务:线程数 ≈ CPU核心数 + 1(防止线程阻塞导致的CPU闲置)
IO密集型任务:线程数 ≈ 2 * CPU核心数(充分利用CPU等待IO的时间)
混合型任务:可考虑将任务分类,使用不同的线程池处理
2. 使用注意事项
避免使用无界队列:可能导致内存溢出,推荐使用ArrayBlockingQueue等有界队列
合理设置线程存活时间:根据任务特性调整keepAliveTime,平衡资源利用与响应速度
自定义线程工厂:为线程设置有意义的名字,便于问题排查
监控线程池状态:通过getActiveCount()、getQueue().size()等方法监控运行状况
避免局部线程池:方法内创建的线程池可能无法被GC回收,导致资源泄漏
3. 预定义线程池与自定义线程池
虽然Executors提供了快速创建线程池的方法,但在生产环境中更推荐使用ThreadPoolExecutor自定义线程池:
// 推荐的自定义线程池实现
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
30, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // 有界队列
new NamedThreadFactory("my-pool"), // 自定义线程工厂
new CustomRejectionPolicy() // 自定义拒绝策略
);
Java线程池原理深度解析
线程池是Java并发编程中的核心组件,它通过池化技术实现了线程的高效管理和复用,显著提升了系统性能和资源利用率。下面我将从线程池的设计思想、核心原理、工作流程、关键参数及实际应用等多个维度进行全面解析。
一、线程池的核心概念与优势
线程池是一种池化技术,其核心思想是预先创建一定数量的线程并保存在池中,当有任务提交时,从池中获取线程执行任务,任务完成后线程返回池中等待下次任务,而非直接销毁。
主要优势包括:
降低资源消耗:通过复用已创建的线程,减少线程频繁创建和销毁的性能开销
提高响应速度:任务到达时可直接使用现有线程执行,无需等待线程创建
提高线程可管理性:统一管理线程生命周期,避免无限制创建线程导致的系统资源耗尽
提供任务队列和拒绝策略:缓冲突增任务,保证系统稳定运行
二、线程池的核心实现原理
1. 核心类与接口
Java线程池基于java.util.concurrent包实现,核心类包括:
Executor:基础执行接口,定义execute()方法
ExecutorService:扩展了Executor,提供更丰富的管理方法如submit()、shutdown()等
ThreadPoolExecutor:线程池的核心实现类,提供完整的线程池功能
Executors:工具类,提供创建预配置线程池的工厂方法
2. 线程池的七大核心参数
ThreadPoolExecutor的构造函数包含七个关键参数,共同决定了线程池的行为特性:
参数 说明 影响
corePoolSize 核心线程数 线程池长期维持的线程数量,即使空闲也不会回收(除非设置allowCoreThreadTimeOut)
maximumPoolSize 最大线程数 线程池允许创建的最大线程数量
keepAliveTime 线程空闲时间 非核心线程空闲超过此时间将被回收
unit 时间单位 空闲时间的单位(秒、毫秒等)
workQueue 任务队列 用于保存待执行任务的阻塞队列
threadFactory 线程工厂 用于创建新线程,可定制线程名称、优先级等
handler 拒绝策略 当线程池和队列都满时的处理策略
3. 线程池的工作流程
线程池处理任务的核心流程如下:
任务提交:通过execute()或submit()方法提交任务
核心线程分配:
若当前线程数 < corePoolSize,立即创建新线程执行任务
否则尝试将任务放入工作队列
队列处理:
若队列未满,任务入队等待执行
若队列已满且线程数 < maximumPoolSize,创建新线程执行任务
拒绝策略:
若队列已满且线程数达到maximumPoolSize,执行拒绝策略
线程复用机制:线程执行完任务后,会通过getTask()方法从队列获取新任务,实现线程的循环利用。
三、线程池的任务队列与拒绝策略
1. 常见任务队列类型
线程池的工作队列决定了任务的缓冲策略,常见实现包括:
ArrayBlockingQueue:基于数组的有界队列,FIFO原则
LinkedBlockingQueue:基于链表的队列,默认无界(可设置容量)
SynchronousQueue:不存储元素的直接交接队列,每个插入操作必须等待移除操作
PriorityBlockingQueue:具有优先级的无界队列
2. 内置拒绝策略
当线程池和队列都饱和时,会触发拒绝策略,JDK提供了四种内置策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常
CallerRunsPolicy:由提交任务的线程自己执行该任务
DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交当前任务
DiscardPolicy:静默丢弃无法处理的任务
开发者也可以实现RejectedExecutionHandler接口自定义拒绝策略。
四、线程池的生命周期与状态管理
线程池内部通过原子变量ctl维护运行状态和线程数量,包含五种状态:
RUNNING:正常运行状态,接受新任务并处理队列任务
SHUTDOWN:调用shutdown()后进入,不再接受新任务但会处理队列中的任务
STOP:调用shutdownNow()后进入,不再接受新任务也不处理队列任务,并中断正在执行的任务
TIDYING:过渡状态,所有任务已终止,workerCount为0,将执行terminated()钩子方法
TERMINATED:terminated()方法执行完成后进入最终状态
关闭方法对比:
shutdown():平缓关闭,等待已提交任务完成
shutdownNow():立即关闭,尝试中断所有任务并返回未执行任务列表
五、线程池的最佳实践
1. 线程池参数配置建议
CPU密集型任务:线程数 ≈ CPU核心数 + 1(防止线程阻塞导致的CPU闲置)
IO密集型任务:线程数 ≈ 2 * CPU核心数(充分利用CPU等待IO的时间)
混合型任务:可考虑将任务分类,使用不同的线程池处理
2. 使用注意事项
避免使用无界队列:可能导致内存溢出,推荐使用ArrayBlockingQueue等有界队列
合理设置线程存活时间:根据任务特性调整keepAliveTime,平衡资源利用与响应速度
自定义线程工厂:为线程设置有意义的名字,便于问题排查
监控线程池状态:通过getActiveCount()、getQueue().size()等方法监控运行状况
避免局部线程池:方法内创建的线程池可能无法被GC回收,导致资源泄漏
3. 预定义线程池与自定义线程池
虽然Executors提供了快速创建线程池的方法,但在生产环境中更推荐使用ThreadPoolExecutor自定义线程池:
// 推荐的自定义线程池实现
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
30, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // 有界队列
new NamedThreadFactory("my-pool"), // 自定义线程工厂
new CustomRejectionPolicy() // 自定义拒绝策略
);
六、线程池的底层实现机制
1. Worker线程封装
### 线程池使用Worker类封装线程,它继承了AbstractQueuedSynchronizer(AQS),具有简单的锁特性:
使用不可重入的独占锁标识线程是否空闲
持有实际执行任务的Thread对象和首个任务
2. 任务执行流程
execute()方法:处理任务提交的核心方法,包含三层判断
addWorker():实际创建线程的方法,通过CAS保证线程数准确增加
runWorker():线程执行任务的循环,不断从队列获取任务执行
processWorkerExit():线程退出时的清理工作
匿名内部类介绍一下,为什么传入的变量要是 final 的
匿名内部类(Anonymous Inner Class)是Java中一种没有显式类名的内部类,它在定义类的同时直接创建其实例,通常用于实现接口或继承抽象类/普通类。其基本语法结构为:
new 父类构造器(参数列表) | 接口() {
// 类体部分:方法实现或覆盖
}
### 具体例子
线程实现(Runnable接口)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行中");
}
}).start();
抽象类/接口的一次性实现
abstract class Animal {
public abstract void sound();
}
public class Test {
public static void main(String[] args) {
Animal cat = new Animal() {
@Override
public void sound() {
System.out.println("喵喵");
}
};
cat.sound(); // 输出:喵喵
}
}
2. 根本原因
这一限制源于变量生命周期不一致的问题:
### 主要还是从内存管理和线程安全的角度考虑
生命周期差异:外部方法的局部变量存在于栈中,方法执行完毕即被销毁;而匿名内部类对象可能存活更久(如被其他代码引用)
值拷贝机制:为了解决生命周期不一致问题,Java编译器会将外部局部变量拷贝一份到内部类中
一致性保证:如果允许修改外部变量或内部类中的拷贝,会导致两者不一致,破坏程序逻
### 为什么必须是final
final修饰符确保了以下两点:, 类似于Muteable, 该值不可修改
基本类型:值不可变,保证拷贝值与原始值始终一致
引用类型:引用地址不可变(但对象内容可变),防止引用被重新赋值导致不一致
解释final 修饰符
final是Java中用于限制修改的关键字,它可以修饰类、方法和变量,被修饰的实体将具有"不可改变"的特性。
合理使用final关键字能提高代码安全性、可读性,并有助于JVM进行优化。
final的基本含义包括:
对于类:表示该类不能被继承
对于方法:表示该方法不能被子类重写(override)
对于变量:表示该变量只能被赋值一次,之后不能修改
从设计意图上看,Java的final关键字从早期版本主要用于性能优化,逐渐转向保障代码严谨性。
当类被声明为final时,这个类不能被继承。这有助于保护类的设计不被修改,防止子类破坏其内部实现。
使用final类的主要场景包括:
工具类或不可变类(如String类)
不是专门为继承而设计的类,类本身的方法之间有复杂的调用关系
出于安全考虑,不得改动的类
创建时确定不会有扩展需求的类
一、编译期的静态检查
语法约束
Java编译器在编译阶段会对final变量进行严格的赋值流分析,确保:
基本类型:一旦初始化后,任何重新赋值的操作都会触发编译错误(如final int a=1; a=2;会直接报错)。
引用类型:禁止对引用变量本身重新赋值(如final List list=new ArrayList(); list=new LinkedList();会编译失败),但允许修改引用对象内部状态(如list.add("item")合法)。
空白final变量的强制初始化
对于未在声明时赋值的final变量(空白final),编译器会强制要求在构造方法或静态代码块中完成初始化,否则报错。这种机制确保了final变量在使用前必然有且仅有一次赋值。
二、运行时的内存屏障与可见性
禁止指令重排序
JMM(Java内存模型)对final域的写入和读取插入特殊的内存屏障:
### 写入屏障:在构造函数中对final域的写入操作,不会被重排序到构造函数之外。这保证了其他线程看到对象引用时,其final域已正确初始化。
### 读取屏障:初次读取包含final域的对象引用时,会确保后续对该final域的读取能看到初始化后的值。
线程安全的发布
若final引用未从构造函数中逸出(即未在构造函数中将this暴露给其他线程),则其他线程看到的该引用指向的对象及其final域的值一定是完全初始化的状态。这种机制避免了因重排序导致的可见性问题。
三、JVM层面的优化与固化
常量池优化
对于编译期常量(如static final int MAX=100),JVM会将其值直接内联到使用处,而非通过变量访问。这既保证了值不可变,又提升了性能。
引用地址固化
引用类型的final变量在初始化后,其指向的堆内存地址会被JVM记录为不可修改。虽然对象内容可变(如数组元素或对象字段),但引用地址的不可变性由JVM通过对象头标记和内存访问控制实现。
GC 根搜索算法,什么样的对象可以做 GC
GC根搜索算法(GC Root Tracing Algorithm)是Java虚拟机判断对象是否存活的核心机制,它通过从一组称为"GC Roots"的特殊对象出发,递归遍历引用链来确定哪些对象应该保留、哪些可以被回收。下面我将详细解析什么样的对象可以作为GC Roots以及根搜索算法的工作原理。
一、GC Roots对象的类型
根据Java虚拟机规范和多篇技术资料,可以作为GC Roots的对象主要包括以下几类:
虚拟机栈中的引用对象
当前执行方法的局部变量和参数
例如:void method() { Object obj = new Object(); }中的obj
方法区中的静态属性引用
类的静态变量引用的对象
例如:public static Object staticObj = new Object();
方法区中的常量引用
字符串常量池中的引用
被final修饰的常量引用的对象
本地方法栈中JNI引用
Native方法引用的Java对象
JNI全局引用和局部引用
活跃线程对象
当前所有正在运行的线程实例
线程栈帧中的局部变量引用的对象
被同步锁持有的对象
被synchronized锁定的对象
例如:synchronized(obj) {...}中的obj
Java虚拟机内部引用
系统类加载器
基本类型对应的Class对象
常驻异常对象(如NullPointerException)
JVM保留的内部对象
Finalizer队列中的对象
等待执行finalize()方法的对象
虽然这些对象即将被回收,但在执行finalize前仍被JVM特殊对待
二、根搜索算法的工作原理
根搜索算法基于可达性分析原理,其工作流程可分为以下几个阶段:
枚举根节点
暂停所有用户线程(Stop-The-World)
收集所有GC Roots对象作为起点
标记可达对象
从GC Roots开始深度优先或广度优先遍历对象图
对所有被引用的对象进行标记
形成从GC Roots到各个对象的引用链
判定不可达对象
未被标记的对象即为不可达对象
这些对象之间可能形成循环引用,但因为没有到GC Roots的路径,仍会被判定为垃圾
回收阶段(根据具体GC算法)
标记-清除:直接回收未标记对象
标记-整理:移动存活对象后回收剩余空间
复制算法:将存活对象复制到新空间
三、对象存活的判定过程 ### 强、弱、虚引用
一个对象从被创建到被回收的生命周期中,会经历多次可达性分析判定:
强可达(Strongly Reachable)
对象可以通过GC Roots的直接引用链访问
例如:Object obj = new Object();中的新创建对象
软可达(Softly Reachable)
仅被SoftReference引用的对象
内存不足时会被回收
弱可达(Weakly Reachable)
仅被WeakReference引用的对象
下次GC时会被回收
虚可达(Phantom Reachable)
仅被PhantomReference引用的对象
随时可能被回收,主要用于回收跟踪
不可达(Unreachable)
无法通过任何引用链访问的对象
将被垃圾回收器回收
问题:Redis 3 台怎么安装?
Redis三台服务器集群安装指南(三主三从配置)
Redis集群模式是Redis官方提供的分布式解决方案,能够提供数据分片、高可用性和负载均衡等功能。下面详细介绍在三台服务器上安装Redis集群(三主三从)的完整步骤。
安装。。。。。。
启动:
在其中任意一台服务器上执行集群创建命令:
redis-cli --cluster create \
10.10.10.100:6379 10.10.10.101:6379 10.10.10.102:6379 \
10.10.10.100:6380 10.10.10.101:6380 10.10.10.102:6380 \
--cluster-replicas 1 -a yourpassword
# 启动主节点
redis-server /usr/local/redis/6379/conf/redis.conf
# 启动从节点
redis-server /usr/local/redis/6380/conf/redis.conf
确定主节点
非集群模式(主从复制),手动故障转移:
redis-cli -h <从节点IP> -p <从节点端口> SLAVEOF NO ONE
哨兵模式(Sentinel)
自动切换,或者
redis-cli -h <哨兵IP> -p <哨兵端口> SENTINEL FAILOVER <主节点名称>
集群模式(Cluster)
redis-cli -h <从节点IP> -p <端口> -a <密码> CLUSTER FAILOVER
场景 推荐方法 适用条件
非集群主从 SLAVEOF NO ONE或FAILOVER 手动维护主从架构
哨兵模式 SENTINEL FAILOVER 需哨兵监控环境
集群模式 CLUSTER FAILOVER或工具修复 确保数据同步完成
根据实际环境选择合适方法,优先使用官方推荐的自动故障转移机制(如哨兵或集群选举),仅在紧急情况下手动强制切换。
原子性(Atomicity)详解
原子性在Java中指的是一个操作或一组操作作为一个不可分割的整体执行,要么全部完成,要么完全不执行,不会出现"部分完成"的中间状态。在多线程环境中,原子性确保了对共享变量的操作不会被其他线程的操作干扰。
1. 原子性的关键特征包括:
不可分割性:原子操作不能被线程调度机制中断
全有或全无:操作要么完全执行,要么完全不执行
无中间状态:其他线程不会看到操作执行到一半的状态
2. volatile关键字的原子性
volatile关键字提供了以下原子性保证:
对volatile变量的单次读/写操作是原子的
保证变量的可见性,写操作会立即刷新到主内存,读操作会从主内存读取
防止指令重排序,保证有序性
3. Synchronized同步机制
synchronized关键字通过互斥锁实现了代码块或方法的原子性:
同一时间只有一个线程可以执行被synchronized保护的代码块
保证了代码块内所有操作的原子性
同时保证了可见性和有序性
### 所以, Atomic 的缺点就是,只能单一原子性变量。不能作用于代码块、方法或者类