每日面试题(虾皮java后端一面)
1.MySQL索引失效的主要场景
1.1违反最左前缀原则
对于复合索引(username, age):
-- 能使用索引
SELECT * FROM users WHERE username = 'john';
SELECT * FROM users WHERE username = 'john' AND age = 25;
-- 不能使用索引(违反最左前缀)
SELECT * FROM users WHERE age = 25;
1.2 在索引列上使用函数或运算
-- 索引失效
SELECT * FROM users WHERE YEAR(create_time) = 2023;
SELECT * FROM users WHERE age + 1 = 26;
-- 可以改写为(使用索引)
SELECT * FROM users WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
1.3 使用不等于(!=或<>)查询
-- 索引失效
SELECT * FROM users WHERE username != 'john';
1.4 使用IS NULL或IS NOT NULL
-- 可能使索引失效(取决于数据分布)
SELECT * FROM users WHERE email IS NULL;
1.5使用LIKE以通配符开头
-- 索引失效
SELECT * FROM users WHERE username LIKE '%ohn';
-- 可以使用索引
SELECT * FROM users WHERE username LIKE 'joh%';
1.6类型转换导致索引失效
-- 如果email是字符串类型,但传入数字
SELECT * FROM users WHERE email = 123; -- 索引失效
-- 应该使用
SELECT * FROM users WHERE email = '123';
1.7 OR条件使用不当
-- 如果age没有索引,整个查询索引失效
SELECT * FROM users WHERE username = 'john' OR age = 25;
1.8使用NOT IN或NOT EXISTS
-- 索引失效
SELECT * FROM users WHERE username NOT IN ('john', 'mary');
1.9数据量少时优化器可能不使用索引
当表中数据量很少时,MySQL可能选择全表扫描而非使用索引。
1.10索引列参与计算
-- 索引失效
SELECT * FROM users WHERE age * 2 = 50;
1.11使用全表扫描比使用索引更快时
MySQL优化器会根据统计信息决定是否使用索引,当预计要访问大量数据时,可能放弃使用索引。
1.12如何避免索引失效
- 遵循最左前缀原则设计和使用复合索引
- 避免在索引列上使用函数或计算
- 合理设计查询语句,避免全表扫描
- 使用EXPLAIN分析查询执行计划
- 定期更新统计信息(ANALYZE TABLE)
2.MySQL聚簇索引与非聚簇索引的区别
2.1聚簇索引(Clustered Index)
-
数据存储方式:
- 索引的叶子节点直接存储完整的数据行
- 表数据本身就是索引的一部分
- 一个表只能有一个聚簇索引
-
特点:
- InnoDB的主键索引就是聚簇索引
- 数据按索引键值的顺序物理存储
- 查询通过主键访问非常高效(只需一次查找)
-
优势:
- 范围查询效率高(相邻数据物理上存储在一起)
- 减少I/O操作(数据与索引在一起)
-
示例:
-- InnoDB表中,PRIMARY KEY就是聚簇索引 CREATE TABLE users ( id INT PRIMARY KEY, -- 聚簇索引 name VARCHAR(50) );
2.2非聚簇索引(Secondary Index/Non-clustered Index)
-
数据存储方式:
- 索引的叶子节点不包含完整数据行
- 存储的是主键值(对于InnoDB)
- 一个表可以有多个非聚簇索引
-
特点:
- 需要二次查找(回表)才能获取完整数据
- 数据物理存储顺序与索引顺序无关
- 普通索引、唯一索引都是非聚簇索引
-
查询过程:
- 先通过非聚簇索引找到主键值
- 再通过主键值到聚簇索引中查找完整数据行
-
示例:
CREATE TABLE users ( id INT PRIMARY KEY, -- 聚簇索引 name VARCHAR(50), INDEX idx_name (name) -- 非聚簇索引 );
2.3主要区别对比
| 特性 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数量限制 | 一个表只能有一个 | 一个表可以有多个 |
| 存储内容 | 存储完整数据行 | 存储主键值(InnoDB) |
| 查询效率 | 主键查询非常高效 | 需要回表操作 |
| 插入速度 | 相对较慢(需要维护物理顺序) | 相对较快 |
| 磁盘空间 | 不额外占用空间 | 需要额外存储空间 |
| 范围查询效率 | 高效(数据物理连续) | 效率较低 |
| 典型示例 | PRIMARY KEY | 普通INDEX/UNIQUE索引 |
| 叶子节点内容 | 数据页 | 主键值+指针 |
2.4实际应用中的注意事项
- InnoDB引擎如果没有显式定义主键:
- 会选择一个唯一的非空索引作为聚簇索引
- 如果没有这样的索引,会隐式创建一个自增的ROWID作为聚簇索引
- 覆盖索引优化:
- 如果查询的列都包含在非聚簇索引中,可以避免回表操作
- 例如:
SELECT id, name FROM users WHERE name = 'John'(如果idx_name包含name和id)
- 主键选择的影响:
- 使用自增ID作为主键有利于顺序插入
- 使用UUID等随机值可能导致页分裂,影响性能
3.MySQL的四种事务隔离级别
3.1 读未提交(Read Uncommitted)
-
特点:事务可以读取其他事务未提交的数据
-
问题:会出现"脏读”(Dirty Read)
-
示例:
-- 事务A UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 此时事务B可以看到这个未提交的修改 -- 如果事务A回滚,事务B读取的就是无效数据(脏数据)
3.2 读已提交(Read Committed)
-
特点:事务只能读取其他事务已提交的数据
-
解决了:脏读问题
-
问题:会出现"不可重复读”(Non-repeatable Read)
-
示例:
-- 事务A第一次查询 SELECT balance FROM accounts WHERE id = 1; -- 返回1000 -- 事务B更新并提交 UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT; -- 事务A第二次查询(同一事务内) SELECT balance FROM accounts WHERE id = 1; -- 返回900(结果不一致)
3.3 可重复读(Repeatable Read) - MySQL默认级别
-
特点:同一事务内多次读取同样数据结果一致
-
解决了:不可重复读问题
-
问题:会出现"幻读”(Phantom Read)
-
示例:
-- 事务A第一次查询 SELECT COUNT(*) FROM accounts WHERE balance > 800; -- 返回10 -- 事务B插入新记录并提交 INSERT INTO accounts(id, balance) VALUES(11, 1000); COMMIT; -- 事务A第二次查询 SELECT COUNT(*) FROM accounts WHERE balance > 800; -- 仍返回10(防止不可重复读) -- 但如果是范围更新,会看到"幻行" UPDATE accounts SET status = 1 WHERE balance > 800; -- 会更新包括事务B新插入的行
3.4 串行化(Serializable)
-
特点:最高隔离级别,完全串行执行
-
解决了:幻读问题
-
问题:性能最低,并发性差
-
实现方式:所有SELECT语句自动转为
SELECT ... FOR SHARE -
示例:
-- 事务A SELECT * FROM accounts WHERE balance > 800; -- 会对记录加共享锁 -- 事务B尝试修改这些记录会被阻塞 UPDATE accounts SET balance = 750 WHERE id = 1; -- 等待事务A提交
MySQL中的实现特点
-
InnoDB在RR级别下通过MVCC+间隙锁:
- 实际上解决了大部分幻读问题
- 纯读操作不会出现幻读
- 写操作通过间隙锁防止幻读
-
隔离级别设置:
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 设置隔离级别(会话级或全局) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -
各隔离级别与问题对照:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| Read Uncommitted | ✓ | ✓ | ✓ | 最高 |
| Read Committed | × | ✓ | ✓ | 高 |
| Repeatable Read | × | × | ✓* | 中 |
| Serializable | × | × | × | 低 |
(* InnoDB的RR级别通过间隙锁基本解决了幻读问题)
4.InnoDB事务实现与MVCC机制详解
4.1 InnoDB事务基础实现
InnoDB通过以下核心组件实现事务:
- 事务ID系统:
- 每个事务启动时会被分配一个唯一的事务ID(trx_id)
- 事务ID是自增的,用于判断事务的先后顺序
- Undo日志:
- 记录数据修改前的状态,用于回滚和MVCC
- 组成版本链,支持多版本并发控制
- Redo日志:
- 记录物理页的修改,保证事务的持久性
- Write-Ahead Logging(WAL)机制
4.2 MVCC(多版本并发控制)核心原理
4.2.1 MVCC核心数据结构
-
隐藏字段:
DB_TRX_ID(6字节):记录创建/最后一次修改该行的事务IDDB_ROLL_PTR(7字节):回滚指针,指向Undo日志中的旧版本DB_ROW_ID(6字节):隐含的自增行ID(当无主键时)
-
ReadView:
struct ReadView { trx_id_t m_low_limit_id; // 高水位:大于等于这个ID的事务都不可见 trx_id_t m_up_limit_id; // 低水位:小于这个ID的事务都可见 trx_id_t m_creator_trx_id; // 创建该ReadView的事务ID ids_t m_ids; // 活跃事务ID列表 // ... };
4.2.2 MVCC工作流程
数据可见性判断规则:
- 比较行的
DB_TRX_ID与ReadView的m_up_limit_id- 如果
DB_TRX_ID<m_up_limit_id,说明在快照前提交,可见
- 如果
- 检查
DB_TRX_ID是否在活跃事务列表(m_ids)中- 如果在,表示创建该版本的事务还未提交,不可见
- 如果不在,且
DB_TRX_ID<m_low_limit_id,说明已提交,可见
- 如果不可见,则通过
DB_ROLL_PTR找到Undo日志中的旧版本,重复检查
不同隔离级别的ReadView生成时机:
- RC(读已提交):每次SELECT都会生成新的ReadView
- RR(可重复读):第一次SELECT时生成ReadView,后续复用
4.2.3 版本链示例
假设有数据行更新历史:
时间线:事务100 → 事务200 → 事务300 → 事务400(当前)
版本链:v1(100) ← v2(200) ← v3(300)
当事务400(trx_id=400)查询时:
- 先访问v3(trx_id=300)
- 300 < 400且不在活跃事务列表 → 可见
- 如果300不可见,则通过roll_ptr找到v2,继续判断
4.3 MVCC与锁的协同工作
- 当前读 vs 快照读:
- 快照读:普通SELECT,使用MVCC
- 当前读:SELECT FOR UPDATE/SHARE, UPDATE, DELETE等使用记录锁
- 防止幻读的间隙锁:
- 在RR级别下,InnoDB通过间隙锁(Gap Lock)防止幻读
- 与MVCC配合实现完整的隔离性
4.4 MVCC的优势与限制
优势:
- 读不加锁,读写不冲突
- 通过版本链支持非阻塞的读操作
- 实现不同隔离级别的语义
限制:
- 需要维护多个数据版本,增加存储开销
- 频繁的UPDATE操作会导致大量Undo日志
- 长期运行的事务可能导致Undo无法清理
5.Redis持久化机制
5.1 RDB (Redis Database)
5.1.1 核心特点
- 全量快照:在指定时间间隔生成数据集的时间点快照
- 二进制格式:紧凑的单一文件,适合备份和灾难恢复
- 高性能:使用子进程进行持久化,主进程继续处理请求
5.1.2 触发方式
-
自动触发:通过配置文件设置保存规则
save 900 1 # 900秒(15分钟)内至少有1个key变化 save 300 10 # 300秒(5分钟)内至少有10个key变化 save 60 10000 # 60秒内至少有10000个key变化 -
手动触发:
SAVE:阻塞主进程直到RDB完成BGSAVE:后台异步执行
5.1.3 工作流程
- 父进程fork子进程
- 子进程将数据集写入临时RDB文件
- 写入完成后替换旧RDB文件
5.1.4 优缺点
优点:
- 文件紧凑,恢复速度快
- 最大化Redis性能(异步方式)
- 适合大规模数据备份
缺点:
- 可能丢失最后一次快照后的数据
- 大数据集时fork过程可能阻塞服务
5.2 AOF (Append Only File)
5.2.1 核心特点
- 日志记录:记录每个写操作命令
- 可读性:文本格式,可通过
redis-check-aof工具修复 - 更安全:最多丢失1秒数据(取决于同步频率)
5.2.2 配置选项
appendonly yes # 启用AOF
appendfilename "appendonly.aof" # 文件名
# 同步策略
appendfsync always # 每个命令都同步,最安全但性能最低
appendfsync everysec # 每秒同步一次(推荐)
appendfsync no # 由操作系统决定
5.2.3 重写机制
- 目的:压缩AOF文件体积,去除冗余命令
- 触发方式:
- 自动:
auto-aof-rewrite-percentage 100(增长100%时触发) - 手动:
BGREWRITEAOF
- 自动:
- 实现原理:
- 创建子进程
- 子进程将当前数据集转换为写命令序列
- 父进程将重写期间的新命令追加到新AOF文件
- 替换旧AOF文件
5.2.4 优缺点
优点:
- 数据安全性更高
- 可恢复性更好
- 日志文件易读易解析
缺点:
- 文件体积通常比RDB大
- 恢复速度较慢
- 写入性能略低于RDB
5.3 混合持久化(Redis 4.0+)
5.3.1 核心特点
-
结合RDB和AOF:AOF文件包含RDB头部和增量AOF日志
-
配置方式:
aof-use-rdb-preamble yes
5.3.2 工作流程
- 重写时先以RDB格式保存当前数据集
- 后续将新命令以AOF格式追加
- 文件前半部分是RDB二进制,后半部分是AOF文本
5.3.3 优势
- 快速加载RDB部分
- 更完整的AOF增量数据
- 兼具RDB的快速恢复和AOF的低丢失特性
5.4 选择策略
| 场景 | 推荐方式 |
|---|---|
| 能容忍数分钟数据丢失 | RDB |
| 需要更高数据安全性 | AOF |
| 需要快速重启恢复 | RDB |
| 需要灾难恢复能力 | RDB+AOF混合 |
| 数据量很大 | RDB(恢复更快) |
5.5 数据恢复流程
- 重启时检查AOF文件是否存在
- 如果存在则加载AOF(混合持久化时先加载RDB部分)
- 如果AOF不存在则加载RDB文件
- 加载完成后开始服务
6.Redis数据类型
6.1 基本数据类型
6.1.1 String(字符串)
-
特点:最基本的数据类型,二进制安全,最大512MB
-
常用命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX] GET key INCR key # 原子递增 DECR key # 原子递减 MSET k1 v1 k2 v2 MGET k1 k2 -
应用场景:
- 缓存HTML片段、JSON数据
- 计数器(文章阅读量)
- 分布式锁(SETNX)
6.1.2 Hash(哈希)
-
特点:键值对集合,适合存储对象
-
常用命令:
HSET user:1000 name "John" age 30 HGET user:1000 name HGETALL user:1000 HINCRBY user:1000 age 1 -
应用场景:
- 存储用户信息、商品信息等对象数据
- 比String更节省空间(针对字段单独存储)
6.1.3 List(列表)
-
特点:有序、可重复的字符串集合,双向链表实现
-
常用命令:
LPUSH list item1 # 左插入 RPUSH list item2 # 右插入 LPOP list # 左弹出 LRANGE list 0 -1 # 获取范围元素 -
应用场景:
- 消息队列(LPUSH+BRPOP)
- 最新消息排行(LPUSH+LTRIM)
- 记录用户操作历史
6.1.4 Set(集合)
-
特点:无序、不重复的字符串集合,自动去重
-
常用命令:
SADD tags redis database SMEMBERS tags SINTER set1 set2 # 交集 SUNION set1 set2 # 并集 -
应用场景:
- 标签系统
- 共同好友(交集)
- 唯一IP统计
6.1.5 ZSet(有序集合)
-
特点:不重复元素集合,每个元素关联一个分数(score)用于排序
-
常用命令:
ZADD leaderboard 100 "player1" 200 "player2" ZRANGE leaderboard 0 -1 WITHSCORES ZREVRANK leaderboard "player1" # 获取排名 -
应用场景:
- 排行榜系统
- 带权重的消息队列
- 范围查询(如时间范围的事件)
6.2 高级数据类型(Redis模块提供)
6.2.1 Bitmaps(位图)
-
本质:String类型的位操作
-
常用命令:
SETBIT online 1001 1 # 用户1001上线 GETBIT online 1001 # 检查是否在线 BITCOUNT online # 统计在线用户数 -
应用场景:
- 用户在线状态
- 每日活跃用户统计
6.2.2 HyperLogLog
-
特点:用于基数统计(不重复元素数量),误差率0.81%
-
常用命令:
PFADD ip_20230501 "192.168.1.1" "192.168.1.2" PFCOUNT ip_20230501 # 估算基数 -
应用场景:
- 大规模UV统计
- 搜索词去重计数
6.2.3 GEO(地理空间)
-
本质:基于ZSet实现的地理位置存储
-
常用命令:
GEOADD cities 116.405285 39.904989 "Beijing" GEODIST cities "Beijing" "Shanghai" km GEORADIUS cities 116 39 100 km # 附近100km的城市 -
应用场景:
- 附近的人/地点
- 地理位置计算
6.2.4 Stream(流,Redis 5.0+)
-
特点:消息队列,支持消费组
-
常用命令:
XADD mystream * sensor-id 1234 temp 19.8 XREAD COUNT 2 STREAMS mystream 0 XGROUP CREATE mystream mygroup $ -
应用场景:
- 消息队列系统
- 事件溯源
6.3 数据类型选择建议
| 需求场景 | 推荐类型 |
|---|---|
| 简单键值存储 | String |
| 对象存储 | Hash |
| 先进先出/后进先出 | List |
| 无序唯一集合 | Set |
| 带排序的集合 | ZSet |
| 布尔值统计 | Bitmap |
| 基数统计 | HyperLogLog |
| 地理位置 | GEO |
| 消息队列 | Stream/List |
6.4 内部编码优化
Redis会根据数据特征自动选择最优的内部编码(可通过OBJECT ENCODING key查看):
- String:int/embstr/raw
- Hash:ziplist/hashtable
- List:quicklist(3.2+)
- Set:intset/hashtable
- ZSet:ziplist/skiplist
7.Redis List底层和时间复杂度
7.1 底层实现演变
Redis List 的底层实现经历了几个重要版本的优化:
7.1.1 Redis 3.2 之前
- 小列表:使用 ziplist(压缩列表)
- 连续内存空间存储
- 适合元素较少且元素较小时(默认配置:元素数<512且元素大小<64字节)
- 大列表:使用 双向链表(linkedlist)
- 每个节点独立分配内存
- 包含指向前后节点的指针(prev/next)
7.1.2 Redis 3.2 及之后版本
- 统一采用 quicklist 结构
- 本质上是 ziplist 组成的双向链表
- 平衡了内存效率和操作性能
7.2、QuickList 详细结构
struct quicklist {
quicklistNode *head; // 头节点指针
quicklistNode *tail; // 尾节点指针
unsigned long count; // 元素总数
unsigned long len; // quicklistNode节点数
int fill : 16; // ziplist大小限制(由list-max-ziplist-size配置)
unsigned int compress : 16; // 压缩深度(由list-compress-depth配置)
};
struct quicklistNode {
quicklistNode *prev; // 前驱指针
quicklistNode *next; // 后继指针
unsigned char *zl; // 指向ziplist的指针
unsigned int sz; // ziplist字节大小
unsigned int count : 16;// ziplist元素计数
unsigned int encoding : 2; // 编码方式(RAW=1, LZF=2)
unsigned int container : 2; // 容器类型(NONE=1, ZIPLIST=2)
unsigned int recompress : 1; // 是否需要重新压缩
};
7.3 操作时间复杂度分析
| 操作命令 | 时间复杂度 | 原理说明 |
|---|---|---|
| LPUSH/RPUSH | O(1) | 在头部/尾部的ziplist插入元素 |
| LPOP/RPOP | O(1) | 从头部/尾部的ziplist弹出元素 |
| LINDEX | O(n) | 可能需要遍历多个ziplist |
| LINSERT | O(n) | 查找位置+插入操作 |
| LRANGE | O(n) | n为返回的元素数量 |
| LTRIM | O(n) | n为保留的元素数量 |
| BLPOP/BRPOP | O(1) | 阻塞弹出操作 |
7.4 查找操作(LINDEX)的详细过程
- 确定节点位置:
- 根据index判断是从头还是从尾开始查找
- 计算目标元素在哪个quicklistNode中
- 在ziplist中查找:
- ziplist是连续内存空间
- 需要遍历才能找到指定位置的元素
- 最坏情况下需要遍历所有ziplist
- 性能优化:
- Redis会记录上次访问的位置(优化连续访问)
- 对于靠近头尾的访问(index=0或-1)有特殊优化
7.5 设计优势
-
内存效率:
- ziplist减少了小元素的内存碎片
- 压缩列表可以节省指针占用的空间(每个元素节省16字节)
-
性能平衡:
- 头部/尾部操作保持O(1)复杂度
- 通过控制ziplist大小限制最坏情况下的性能
-
可配置性:
list-max-ziplist-size -2 # 单个ziplist大小限制(-2表示8KB) list-compress-depth 1 # 两端不压缩的节点数
7.6 与其它数据结构对比
| 特性 | QuickList | 纯LinkedList | 纯Ziplist |
|---|---|---|---|
| 内存使用 | 中等 | 最高 | 最低 |
| LPUSH/LPOP | O(1) | O(1) | O(1) |
| 中间插入 | O(n) | O(1) | O(n) |
| 随机访问 | O(n) | O(n) | O(n) |
| 大元素支持 | 是 | 是 | 否 |
8.操作系统通信方式
8.1 进程间通信(IPC)方式
8.1.1 管道(Pipe)
-
特点:
- 单向通信,半双工
- 只能用于父子进程或兄弟进程间
- 基于字节流传输
-
类型:
- 匿名管道:
int pipe(int fd[2]) - 命名管道(FIFO):
mkfifo命令创建
- 匿名管道:
-
示例:
int fd[2]; pipe(fd); // fd[0]读端,fd[1]写端
8.1.2 消息队列(Message Queue)
-
特点:
- 消息链表结构,存储在内核
- 按消息类型读取,克服了管道只能承载无格式字节流的缺点
- POSIX和System V两种标准
-
相关函数:
msgget() // 创建/获取队列 msgsnd() // 发送消息 msgrcv() // 接收消息
8.1.3 共享内存(Shared Memory)
- 特点:
- 最高效的IPC方式,进程直接访问同一块内存
- 需要同步机制配合(如信号量)
- 实现步骤:
- 创建共享内存段
shmget() - 附加到进程地址空间
shmat() - 分离
shmdt() - 控制
shmctl()
- 创建共享内存段
8.1.4 信号量(Semaphore)
-
作用:
- 用于进程间同步,不用于数据传输
- 解决资源共享的互斥访问
-
操作:
semget() // 创建/获取信号量集 semop() // P/V操作
8.1.5 信号(Signal)
- 特点:
- 异步通知机制,用于通知进程发生了某事件
- 如
SIGKILL,SIGTERM等
- 处理方式:
- 忽略信号
- 捕获并处理
- 执行默认动作
8.1.6 套接字(Socket)
- 特点:
- 可用于不同主机间的进程通信
- 全双工通信
- 类型:
- 流式套接字(SOCK_STREAM)
- 数据报套接字(SOCK_DGRAM)
8.1.7 内存映射(Memory Mapping)
-
原理:
- 将文件映射到进程地址空间
- 多个进程可映射同一文件实现共享
-
函数:
mmap() // 创建映射 munmap() // 解除映射
8.2 线程间通信方式
8.2.1 共享变量
- 通过全局变量或堆内存共享数据
- 需要配合同步机制(互斥锁等)
8.2.2 互斥锁(Mutex)
pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);
8.2.3 条件变量(Condition Variable)
pthread_cond_wait(&cond, &mutex); // 等待条件
pthread_cond_signal(&cond); // 通知等待线程
8.2.4 读写锁(Read-Write Lock)
pthread_rwlock_rdlock(&lock); // 读锁
pthread_rwlock_wrlock(&lock); // 写锁
8.2.5 线程信号量(Semaphore)
- 类似进程信号量,但更轻量级
sem_init(&sem, 0, 1); // 初始化
sem_wait(&sem); // P操作
sem_post(&sem); // V操作
8.3 通信方式对比
| 通信方式 | 适用场景 | 数据传输 | 同步需求 | 性能 |
|---|---|---|---|---|
| 管道 | 父子进程简单通信 | 单向 | 无 | 中 |
| 消息队列 | 进程间结构化数据传输 | 双向 | 可选 | 中 |
| 共享内存 | 高性能大数据量交换 | 直接访问 | 需要 | 最高 |
| 信号量 | 进程/线程同步 | 无 | 专门用于 | 高 |
| 套接字 | 跨网络通信 | 全双工 | 需要 | 较低 |
| 互斥锁 | 线程间互斥访问 | 无 | 专门用于 | 很高 |
8.4 选择建议
- 高性能场景:共享内存+信号量
- 结构化数据:消息队列
- 简单通信:管道
- 跨主机通信:套接字
- 线程同步:互斥锁/条件变量
9.死锁产生的条件
9.1 四个必要条件(Coffman条件)
9.1.1 互斥条件(Mutual Exclusion)
- 定义:资源一次只能由一个进程/线程占有,其他请求者必须等待
- 示例:打印机、共享变量等排他性资源
- 特点:某些资源本质就是互斥的,无法同时共享
9.1.2 占有并等待(Hold and Wait)
- 定义:进程已经持有至少一个资源,同时又在等待获取其他被占用的资源
- 示例:
- 进程A持有资源1,请求资源2
- 进程B持有资源2,请求资源1
9.1.3 非抢占条件(No Preemption)
- 定义:已分配给进程的资源不能被其他进程强行夺取,必须由进程自行释放
- 特点:
- 资源只能自愿释放
- 操作系统不能强行回收已分配资源
9.1.4 循环等待条件(Circular Wait)
-
定义:存在一个进程等待的循环链,每个进程都在等待下一个进程所占用的资源
-
示例:
进程A → 持有资源1,等待资源2 进程B → 持有资源2,等待资源1 -
表现形式:等待图中存在环
10.select、poll、epoll的区别
10.1 基本概念对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 出现时间 | BSD 4.2 (1983) | System V (1986) | Linux 2.5.44 (2002) |
| 实现机制 | 轮询 | 轮询 | 回调通知 |
| 时间复杂度 | O(n) | O(n) | O(1) |
| 最大连接数 | FD_SETSIZE(通常1024) | 无硬限制 | 无硬限制 |
| 工作模式 | 水平触发(LT) | 水平触发(LT) | 支持LT和边缘触发(ET) |
| 内核支持 | 所有平台 | 所有平台 | Linux特有 |
10.2 底层实现原理
10.2.1 select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// 工作流程:
// 1. 用户态拷贝fd_set到内核
// 2. 内核线性扫描所有fd
// 3. 标记可读写状态
// 4. 拷贝修改后的fd_set回用户态
问题:
- 每次调用需要重置fd_set
- 每次都需要全量拷贝
- 遍历复杂度O(n)
10.2.2 poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 监听的事件
short revents; // 返回的事件
};
改进:
- 使用pollfd数组,突破1024限制
- 分离了监听事件和返回事件
- 但仍然需要轮询所有fd
10.2.3 epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
// 核心改进:
// 1. 使用红黑树管理fd(O(logN)查找)
// 2. 就绪列表保存活跃fd
// 3. 回调机制通知内核事件
10.3 性能对比
连接数 vs 性能
- 少量连接:三者性能相当
- 万级连接:epoll优势明显
- 十万级连接:select/poll基本不可用
关键差异点
| 操作 | select/poll | epoll |
|---|---|---|
| 每次调用fd传递方式 | 全量拷贝 | 通过epoll_ctl增量管理 |
| 事件检测机制 | 线性扫描 | 回调通知 |
| 活跃连接处理 | 返回所有fd,用户需要遍历判断 | 只返回就绪fd列表 |
| 内存使用 | 每次调用需要重新传入所有fd | 内核维护持久化数据结构 |
10.4 边缘触发(ET) vs 水平触发(LT)
epoll特有模式:
struct epoll_event {
uint32_t events; // EPOLLIN | EPOLLET 表示边缘触发
epoll_data_t data;
};
| 模式 | 特点 | 应用场景 |
|---|---|---|
| 水平触发(LT) | 只要fd就绪就会一直通知,直到数据被处理完 | 默认模式,编程更简单 |
| 边缘触发(ET) | 只在fd状态变化时通知一次,必须一次性处理完所有数据 | 高性能场景,需要更精细的控制 |
ET模式注意事项:
- 必须使用非阻塞IO
- 必须一次性读完所有数据(直到read返回EAGAIN)
- 容易遗漏事件,需要更严谨的代码逻辑
10.5 编程接口对比
典型select代码
fd_set read_fds;
while(1) {
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
select(sockfd+1, &read_fds, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &read_fds)) {
// 处理数据
}
}
典型epoll代码
int epfd = epoll_create(1);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while(1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i = 0; i < nfds; i++) {
if(events[i].data.fd == sockfd) {
// 处理数据
}
}
}
10.6 选型建议
- 跨平台需求:使用poll(比select更现代)
- Linux高并发:必须使用epoll
- 低延迟场景:epoll+ET模式
- 简单应用:select/poll足够
11.http1.0和2.0的区别
11.1 基础架构对比
| 特性 | HTTP/1.0 | HTTP/2.0 |
|---|---|---|
| 发布时间 | 1996 | 2015 |
| 传输协议 | 明文文本 | 二进制分帧 |
| 连接方式 | 短连接(默认) | 长连接(多路复用) |
| 头部压缩 | 无 | 使用HPACK压缩 |
| 服务器推送 | 不支持 | 支持 |
| 请求优先级 | 无 | 可设置优先级 |
11.2 关键差异详解
11.2.1 多路复用 (Multiplexing)
-
HTTP/1.0问题:
- 每个请求需要单独TCP连接(开启Keep-Alive后略改善)
- 线头阻塞(Head-of-Line Blocking):顺序处理请求
-
HTTP/2.0解决方案:
graph TD A[流1] -->|帧1| B[TCP连接] C[流2] -->|帧2| B D[流3] -->|帧3| B- 单个TCP连接上并行交错传输多个请求/响应
- 通过二进制分帧实现(数据拆分为帧,带流ID标识)
11.2.2 二进制分帧层
-
HTTP/1.0:
GET /index.html HTTP/1.0 Host: example.com纯文本格式,按行解析
-
HTTP/2.0:
+-----------------------------------------------+ | Length (24) | Type (8) | Flags (8) | R (1) | Stream ID (31) | | Frame Payload (24) | +-----------------------------------------------+二进制格式帧结构,更高效解析
11.2.3 头部压缩 (HPACK)
- HTTP/1.0问题:
- 每次请求重复发送相同头部(Cookie/User-Agent等)
- 头部未压缩,占用大量带宽
- HTTP/2.0解决方案:
- 使用HPACK算法压缩
- 维护静态表(61个常用头字段)和动态表
- 差分编码减少重复传输
11.2.4 服务器推送 (Server Push)
sequenceDiagram
Client->>Server: 请求/index.html
Server->>Client: 返回/index.html + 推送style.css
Server->>Client: 推送script.js
- 服务端可主动推送资源,减少往返延迟
- 客户端可通过RST_STREAM拒绝推送
11.2.5请求优先级
-
可设置流的依赖关系和权重
Stream 1: 依赖根流,权重=16 Stream 2: 依赖流1,权重=8 -
确保关键资源优先传输
11.3 性能对比示例
加载包含20个资源的页面:
| 指标 | HTTP/1.0 (6个TCP连接) | HTTP/2.0 |
|---|---|---|
| 总连接数 | 6 | 1 |
| 总传输数据量 | 32KB (含重复头部) | 18KB |
| 完成时间 | 600ms | 350ms |
11.4 兼容性与部署
- 协议协商机制:
- TLS使用ALPN扩展
- 明文HTTP通过Upgrade头部协商
- 必须使用HTTPS吗?
- 标准不强制,但所有主流浏览器只支持HTTP/2 over TLS
- 降级处理:
- 当客户端不支持时,服务器自动回退到HTTP/1.x
11.5 实际应用建议
- 迁移到HTTP/2.0:
- 无需修改应用代码(二进制分帧对应用层透明)
- 但可优化资源加载策略(减少文件合并)
- 优化方向:
- 减少域名分片(HTTP/2下多个域名反而降低性能)
- 停止使用雪碧图/合并脚本(充分利用多路复用)
- 合理使用服务器推送(避免过度推送)
12.对称加密常用的算法
12.1 主流对称加密算法
12.1.1 AES (Advanced Encryption Standard)
- 密钥长度:128/192/256位
- 分组大小:128位
- 轮数:10/12/14(取决于密钥长度)
- 特点:
- 目前最安全的对称加密标准
- 取代了旧的DES标准
- 被美国政府选为联邦信息处理标准(FIPS)
- 工作模式:
- ECB(不推荐)
- CBC(最常用)
- GCM(带认证的加密)
12.1.2 DES (Data Encryption Standard)
- 密钥长度:56位(已不安全)
- 分组大小:64位
- 轮数:16
- 现状:
- 已被证明可暴力破解
- 仅用于遗留系统
- 3DES是其改进版(三个DES组合)
12.1.3 3DES (Triple DES)
- 密钥长度:168位(实际安全强度约112位)
- 加密过程:加密→解密→加密(EDE模式)
- 特点:
- 比DES更安全
- 性能比AES差
- 正在被逐步淘汰
12.1.4 ChaCha20
- 密钥长度:256位
- 特点:
- 由Google设计
- 移动设备上性能优于AES
- 与Poly1305认证结合使用
- TLS 1.3标准支持
12.1.5 RC4 (已弃用)
- 类型:流密码
- 密钥长度:40-2048位
- 安全问题:
- 已被证明存在严重漏洞
- 2015年被RFC禁止在TLS中使用
12.2 算法对比表
| 算法 | 密钥长度 | 安全性 | 性能 | 适用场景 | 现状 |
|---|---|---|---|---|---|
| AES | 128/192/256 | ★★★★★ | ★★★★☆ | 通用加密 | 主流标准 |
| ChaCha20 | 256 | ★★★★★ | ★★★★★ | 移动设备、网络传输 | 新兴趋势 |
| 3DES | 168 | ★★☆☆☆ | ★★☆☆☆ | 遗留系统 | 逐步淘汰 |
| DES | 56 | ★☆☆☆☆ | ★★★☆☆ | 历史研究 | 已淘汰 |
| RC4 | 40-2048 | ☆☆☆☆☆ | ★★★★★ | - | 已禁止使用 |
12.3 工作模式选择
1. CBC (Cipher Block Chaining)
- 需要初始化向量(IV)
- 并行度低
- 易受填充预言攻击
2. GCM (Galois/Counter Mode)
- 认证加密(AEAD)
- 支持并行计算
- TLS 1.2+推荐模式
3. ECB (Electronic Codebook)
- 简单但不安全
- 相同明文生成相同密文
- 仅适用于随机数据加密
12.4 代码示例
AES-CBC加密示例(Python)
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
key = get_random_bytes(16) # AES-128
iv = get_random_bytes(16)
# 加密
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = b"Hello World!"
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
# 解密
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
ChaCha20-Poly1305示例(Go)
package main
import (
"crypto/rand"
"golang.org/x/crypto/chacha20poly1305"
)
func main() {
key := make([]byte, chacha20poly1305.KeySize)
rand.Read(key)
aead, _ := chacha20poly1305.New(key)
nonce := make([]byte, aead.NonceSize())
plaintext := []byte("Hello World!")
ciphertext := aead.Seal(nil, nonce, plaintext, nil)
decrypted, _ := aead.Open(nil, nonce, ciphertext, nil)
}
12.5 选型建议
- 通用场景:AES-256-GCM
- 移动/Web:ChaCha20-Poly1305
- 遗留系统:3DES(尽快迁移)
- 避免使用:DES、RC4、ECB模式
13.HTTPS的加密方式,以及如何通信
13.1 HTTPS使用的加密技术
13.1.1 混合加密体系
HTTPS采用对称加密和非对称加密相结合的方式:
| 加密类型 | 用途 | 常用算法 |
|---|---|---|
| 非对称加密 | 密钥交换和身份验证 | RSA、ECDSA、DH |
| 对称加密 | 数据传输加密 | AES-128/256、ChaCha20 |
| 散列算法 | 完整性校验 | SHA-256、SHA-384 |
13.1.2 核心加密组件
- SSL/TLS协议:安全传输层协议(现主要使用TLS 1.2/1.3)
- 数字证书:由CA签发,验证服务器身份
- 密钥交换:ECDHE或RSA密钥交换
13.2 HTTPS完整通信流程
13.2.1 TCP三次握手
客户端 → 服务器:SYN
客户端 ← 服务器:SYN-ACK
客户端 → 服务器:ACK
13.2.2 TLS握手(以TLS 1.2为例)
步骤1:Client Hello
客户端 → 服务器:
- 支持的TLS版本
- 客户端随机数(random_C)
- 支持的加密套件列表
- SNI(服务器名称指示)
步骤2:Server Hello
客户端 ← 服务器:
- 选择的TLS版本
- 服务器随机数(random_S)
- 选择的加密套件
- 服务器证书(包含公钥)
- (可选)证书请求
步骤3:验证证书
- 客户端验证证书:
- 证书链验证
- 有效期检查
- CRL/OCSP吊销检查
- 域名匹配验证
步骤4:密钥交换
客户端 → 服务器:
- 预主密钥(pre_master_secret)(用服务器公钥加密)
- (ECDHE)客户端密钥交换参数
步骤5:生成会话密钥
双方通过以下参数生成相同的会话密钥:
master_secret = PRF(pre_master_secret, "master secret", random_C + random_S)
步骤6:握手完成
客户端 ←→ 服务器:
- 切换加密通信
- 验证握手消息完整性
13.2.3 加密数据传输
使用协商好的对称密钥加密HTTP数据:
加密数据 = AES(HTTP数据, 会话密钥)
13.3 TLS 1.3的改进(2018年发布)
- 简化握手:握手时间减少到1-RTT(甚至0-RTT)
- 移除不安全算法:去除RSA密钥传输、SHA-1、CBC模式等
- 前向安全:默认使用ECDHE密钥交换
- 加密SNI:解决隐私泄露问题
13.4 密钥交换过程详解(ECDHE为例)
sequenceDiagram
Client->>Server: ClientHello (支持ECDHE)
Server->>Client: ServerHello + ECDHE参数 + 证书
Client->>Server: ECDHE客户端参数
Note over Client,Server: 双方独立计算得出相同的会话密钥
- 服务器生成椭圆曲线参数:
(curve, G, pub_S = d_S * G) - 客户端生成:
pub_C = d_C * G - 双方计算共享密钥:
shared_key = d_C * pub_S = d_S * pub_C
13.5 数字证书验证流程
- 获取证书链:从服务器证书到根CA证书
- 验证签名:用上级CA公钥验证下级证书签名
- 检查有效期:证书是否在有效期内
- 检查吊销状态:通过CRL或OCSP协议
- 域名匹配:确保证书中的域名与访问域名一致
13.6 HTTPS数据包结构示例
| IP头部 | TCP头部 | TLS记录层 | 加密的HTTP数据 |
↓
TLS记录层包含:
| 内容类型 | 版本 | 长度 | 加密数据 |
13.7 常见加密套件解析
示例:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- 密钥交换:ECDHE
- 身份验证:RSA
- 对称加密:AES-128-GCM
- 散列算法:SHA-256
13.8 部署建议
-
证书选择:
- 使用RSA 2048位或ECC 256位
- 选择受信任的CA机构
-
协议配置:
- 禁用TLS 1.0/1.1
- 优先使用TLS 1.3
-
加密套件顺序:
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256';
14.四次挥手的过程、为啥要等待2MSL
14.1 四次挥手完整过程
sequenceDiagram
participant Client as 主动关闭方(客户端)
participant Server as 被动关闭方(服务器)
Client->>Server: FIN=1, seq=u (FIN_WAIT_1)
Server->>Client: ACK=1, ack=u+1 (CLOSE_WAIT)
Server->>Client: FIN=1, seq=v, ack=u+1 (LAST_ACK)
Client->>Server: ACK=1, ack=v+1 (TIME_WAIT)
详细步骤说明:
- 第一次挥手(FIN_WAIT_1):
- 主动关闭方发送FIN报文,序列号为u
- 进入FIN_WAIT_1状态
- 第二次挥手(CLOSE_WAIT):
- 被动关闭方收到FIN,回复ACK确认(ack=u+1)
- 进入CLOSE_WAIT状态
- 主动关闭方收到ACK后进入FIN_WAIT_2状态
- 第三次挥手(LAST_ACK):
- 被动关闭方处理完剩余数据后,发送FIN报文,序列号为v
- 进入LAST_ACK状态
- 第四次挥手(TIME_WAIT):
- 主动关闭方收到FIN后,发送ACK确认(ack=v+1)
- 进入TIME_WAIT状态,等待2MSL后关闭
- 被动关闭方收到ACK后立即关闭连接
14.2 TIME_WAIT状态存在的必要性
1. 确保最后一个ACK到达对端(可靠性)
-
如果最后一个ACK丢失,被动关闭方会重传FIN
-
主动关闭方在TIME_WAIT期间能响应这个重传的FIN
-
如果没有TIME_WAIT:
graph LR A[ACK丢失] --> B[被动方重传FIN] B --> C[主动方已关闭无法响应] C --> D[被动方无法正常关闭]
2. 让网络中残留的旧报文失效(避免混淆)
- MSL(Maximum Segment Lifetime):报文最大生存时间(通常30秒-2分钟)
- 等待2MSL确保两个方向上的旧报文都从网络中消失
- 防止旧连接的延迟报文被新连接误接收
14.3 2MSL等待时间的计算
-
MSL定义:RFC 793建议为2分钟,实际实现常为30秒/60秒
-
2MSL时长:
- Linux:60秒(MSL=30秒)
- Windows:240秒(MSL=120秒)
-
计算公式:
TIME_WAIT持续时间 = 2 × MSL
14.4 TIME_WAIT的影响与优化
高并发场景问题
- 大量连接处于TIME_WAIT状态
- 占用端口资源,可能导致新连接无法建立
优化方案
-
内核参数调整:
# 减少TIME_WAIT时间(Linux) echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout # 启用TIME_WAIT重用 echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse -
连接池管理:
- 避免频繁创建短连接
- 使用长连接减少握手/挥手次数
-
SO_LINGER选项:
struct linger so_linger; so_linger.l_onoff = 1; // 启用 so_linger.l_linger = 0; // 直接发送RST跳过TIME_WAIT setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
14.5 异常情况处理
1. 主动关闭方崩溃
- 未发送最后一个ACK:被动关闭方等待重传超时后关闭
2. 被动关闭方崩溃
- 主动关闭方在FIN_WAIT_2状态有超时机制(tcp_fin_timeout)
3. 同时关闭
sequenceDiagram
participant A as 主机A
participant B as 主机B
A->>B: FIN
B->>A: FIN
A->>B: ACK
B->>A: ACK
双方都经历FIN_WAIT和CLOSING状态
14.6 与三次握手的对比
| 特性 | 三次握手 | 四次挥手 |
|---|---|---|
| 发起阶段 | 连接建立 | 连接终止 |
| 初始状态 | CLOSED → SYN_SENT | ESTABLISHED → FIN_WAIT_1 |
| 关键状态 | SYN_RCVD | TIME_WAIT |
| 数据交换 | 可携带数据(SYN包除外) | FIN包不携带数据 |
| 设计目的 | 同步初始序列号 | 可靠关闭和资源清理 |
15.多线程的实现
15.1 基础实现方式
15.1.1 继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
// 使用
MyThread thread = new MyThread();
thread.start(); // 启动线程
15.1.2 实现Runnable接口(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable线程: " + Thread.currentThread().getName());
}
}
// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();
15.1.3 实现Callable接口(带返回值)
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable结果: " + Thread.currentThread().getName();
}
}
// 使用
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get(); // 获取返回值
15.2 线程池实现(Executor框架)
15.2.1 常用线程池类型
// 1. 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 可缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 3. 单线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 4. 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
15.2.2 使用示例
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交Runnable任务
pool.execute(() -> {
System.out.println("Lambda Runnable: " + Thread.currentThread().getName());
});
// 提交Callable任务
Future<String> future = pool.submit(() -> {
return "Lambda Callable: " + Thread.currentThread().getName();
});
// 关闭线程池
pool.shutdown();
15.3 Java并发工具类
15.3.1 同步控制
// 1. synchronized关键字
public synchronized void syncMethod() { /*...*/ }
// 2. ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
// 3. Semaphore
Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
semaphore.acquire();
try {
// 受限资源访问
} finally {
semaphore.release();
}
15.3.2 线程协作
// 1. CountDownLatch
CountDownLatch latch = new CountDownLatch(3);
// 主线程等待
latch.await();
// 工作线程完成时
latch.countDown();
// 2. CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障");
});
// 工作线程中
barrier.await();
// 3. Phaser (更灵活的屏障)
Phaser phaser = new Phaser(3); // 注册3个线程
phaser.arriveAndAwaitAdvance();
15.4 线程安全集合
15.4.1 并发集合类
// 1. ConcurrentHashMap
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
// 2. CopyOnWriteArrayList
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
// 3. BlockingQueue
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(10);
blockingQueue.put("item"); // 阻塞插入
String item = blockingQueue.take(); // 阻塞获取
15.4.2 使用示例
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
15.5 Java内存模型(JMM)与volatile
15.5.1 volatile关键字
private volatile boolean running = true;
public void stop() {
running = false; // 对其他线程立即可见
}
15.5.2 原子类
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作
15.6 最佳实践
-
优先选择线程池而非直接创建线程
-
实现Runnable/Callable优于继承Thread
-
使用并发集合替代同步集合(如Vector/Hashtable)
-
合理使用锁:
- 减小同步代码块范围
- 避免嵌套锁
-
处理异常:
ExecutorService pool = Executors.newFixedThreadPool(1); pool.submit(() -> { try { // 任务代码 } catch (Exception e) { // 处理异常 } });
15.7 Java 8+新特性
1. CompletableFuture
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenAccept(System.out::println);
2. 并行流
List<String> list = Arrays.asList("a", "b", "c");
list.parallelStream().forEach(System.out::println);
16.垃圾回收算法
16.1 基础垃圾回收算法
16.1.1 标记-清除算法(Mark-Sweep)
工作原理:
- 标记阶段:从GC Roots出发,标记所有可达对象
- 清除阶段:回收未被标记的对象占用的内存
特点:
- 会产生内存碎片
- 执行效率不稳定(堆越大,耗时越长)
- 适用于老年代(如CMS回收器的老年代回收)
16.1.2 复制算法(Copying)
工作原理:
- 将内存分为大小相同的两块
- 只使用其中一块,当这块用完时
- 将存活对象复制到另一块
- 清理已使用的内存空间
特点:
- 没有内存碎片
- 浪费一半内存空间
- 适用于新生代(如Serial、ParNew等新生代回收器)
- 实际JVM实现中,新生代按8:1:1比例分为Eden和两个Survivor区
16.1.3 标记-整理算法(Mark-Compact)
工作原理:
- 标记阶段:与标记-清除相同
- 整理阶段:将所有存活对象向一端移动
- 清理边界以外的内存
特点:
- 避免内存碎片
- 移动对象需要时间
- 适用于老年代(如Serial Old、Parallel Old)
16.1.4 分代收集理论
内存划分:
- 新生代(Young Generation):新创建的对象
- Eden区
- Survivor区(From/To)
- 老年代(Old Generation):长期存活的对象
- 永久代/元空间(PermGen/Metaspace):类元数据等
回收策略组合:
- 新生代:通常使用复制算法
- 老年代:通常使用标记-清除或标记-整理算法
16.2 现代垃圾收集器
16.2.1 Serial收集器
- 单线程收集器
- 新生代使用复制算法,老年代使用标记-整理算法
- Client模式下的默认收集器
-XX:+UseSerialGC显式指定
16.2.2 ParNew收集器
- Serial收集器的多线程版本
- 新生代并行收集,老年代串行
- CMS的默认新生代搭档
-XX:+UseParNewGC指定
16.2.3 Parallel Scavenge收集器
- 关注吞吐量(用户代码时间/(用户代码时间+GC时间))
- 新生代使用复制算法,老年代使用标记-整理
-XX:+UseParallelGC或-XX:+UseParallelOldGC
16.2.4 CMS(Concurrent Mark Sweep)收集器
四阶段过程:
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
特点:
- 低停顿时间为目标
- 使用标记-清除算法,会产生碎片
-XX:+UseConcMarkSweepGC启用
16.2.5 G1(Garbage First)收集器
Region分区模型:
- 将堆划分为多个大小相等的Region(1MB~32MB)
- 新生代和老年代不再是物理隔离
回收过程:
- 初始标记(STW)
- 并发标记
- 最终标记(STW)
- 筛选回收(Evacuation)
特点:
- 可预测的停顿时间模型
- 整体基于标记-整理,局部基于复制算法
- JDK9+默认收集器
-XX:+UseG1GC启用
16.2.6 ZGC和Shenandoah
新一代低延迟收集器:
- 目标:停顿时间不超过10ms
- 关键技术:
- 染色指针(Colored Pointers)
- 读屏障(Read Barriers)
- 适用于超大堆内存(8TB+)
-XX:+UseZGC或-XX:+UseShenandoahGC
16.3 关键概念与调优参数
1. 重要概念
- Stop-The-World(STW):GC时暂停所有应用线程
- 安全点(Safepoint):GC时线程暂停的位置
- 卡表(Card Table):记录老年代对新生代引用的数据结构
- 记忆集(Remembered Set):记录跨代引用的数据结构
2. 常用JVM参数
# 堆内存设置
-Xms4g -Xmx4g # 初始和最大堆大小
-XX:NewRatio=2 # 老年代/新生代比例
-XX:SurvivorRatio=8 # Eden/Survivor比例
# GC日志
-XX:+PrintGCDetails -Xloggc:gc.log
# 收集器选择
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
# 调优参数
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(G1)
-XX:G1HeapRegionSize=8m # G1 Region大小
-XX:InitiatingHeapOccupancyPercent=45 # G1触发并发标记的堆占用率
16.4 GC触发条件
1. Minor GC/Young GC
- 触发条件:Eden区满
- 特点:频率高,速度快
2. Major GC/Full GC
- 触发条件:
- 老年代空间不足
- 方法区空间不足
- System.gc()调用(不一定立即执行)
- CMS GC失败后的担保失败
- 特点:停顿时间长,应尽量避免
16.5 选择策略
| 场景 | 推荐收集器 | 原因 |
|---|---|---|
| 客户端应用 | Serial | 简单高效 |
| 注重吞吐量 | Parallel Scavenge | 高吞吐 |
| 注重低延迟 | CMS/G1 | 减少停顿 |
| 超大堆内存 | G1/ZGC | 可扩展性 |
| 云原生环境 | Shenandoah | 低延迟 |
17.标记清除流程
17.1 算法核心步骤
17.1.1 标记阶段(Mark Phase)
graph TD
A[GC Roots] -->|引用| B[对象A]
A -->|引用| C[对象B]
B -->|引用| D[对象C]
C -->|引用| E[对象D]
具体过程:
- 暂停应用程序线程(Stop-The-World)
- 从GC Roots出发,遍历对象引用图
- GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- GC Roots包括:
- 对每个可达对象打上标记(通常在对象头中记录)
17.1.2 清除阶段(Sweep Phase)
graph LR
A[已标记对象] --> B[保留]
C[未标记对象] --> D[回收内存]
具体过程:
- 线性遍历堆内存中的所有对象
- 对于未被标记的对象,将其所在内存块记录到空闲列表(Free List)中
- 清除所有对象的标记位(为下次GC做准备)
- 恢复应用程序线程
17.2 内存布局示例
回收前堆状态:
[对象A][对象B][对象C][对象D][对象E]
↑ ↑ × ↑ ×
(标记) (标记) (未标记) (标记) (未标记)
回收后堆状态:
[对象A][对象B][空闲][对象D][空闲]
17.3 算法特点分析
优点:
- 实现简单:逻辑直观,易于实现
- 不移动对象:适合存活对象多的场景(如老年代)
缺点:
- 内存碎片:会产生不连续的内存空间
- 可能导致后续大对象无法分配(即使总空闲足够)
- 效率问题:
- 标记阶段需要遍历所有存活对象(与存活对象数量正相关)
- 清除阶段需要遍历整个堆(与堆大小正相关)
- STW停顿:两个阶段都需要暂停应用线程
17.4 实际应用中的改进
1. 标记优化技术
- 三色标记法:
- 白色:未访问
- 灰色:已访问但子引用未处理完
- 黑色:已访问且子引用已处理
- 增量更新(CMS使用):
- 记录并发标记期间新增的引用
- 原始快照(G1/Shenandoah使用):
- 以标记开始时为快照进行扫描
2. 清除优化技术
- 空闲列表管理:
- 按内存块大小维护多个空闲列表
- 分配时选择最合适大小的块
- 碎片整理:
- 后续的标记-整理算法解决此问题
17.5 与其它算法的关系
- 标记-整理算法:在标记-清除基础上增加整理阶段
- 分代收集:老年代常使用标记-清除或其变种
- CMS回收器:老年代并发标记-清除的代表实现
17.6 典型应用场景
- CMS回收器的老年代回收(除并发失败后的Full GC)
- 大对象分配的专用内存区域
- 早期JVM的主要回收算法
18.手撕:二维数组的二分查找
18.1 行列均有序的二维数组(LeetCode 240题)
方法一:从右上角开始的搜索
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int rows = matrix.length;
int cols = matrix[0].length;
int row = 0;
int col = cols - 1; // 从右上角开始
while (row < rows && col >= 0) {
if (matrix[row][col] == target) {
return true;
} else if (matrix[row][col] < target) {
row++; // 排除当前行
} else {
col--; // 排除当前列
}
}
return false;
}
}
时间复杂度:O(m + n),其中m是行数,n是列数
方法二:二分查找优化
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int rows = matrix.length;
int cols = matrix[0].length;
int left = 0;
int right = rows * cols - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
int midValue = matrix[mid / cols][mid % cols];
if (midValue == target) {
return true;
} else if (midValue < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
}
时间复杂度:O(log(mn)),标准的二分查找复杂度
18.2 严格有序的二维数组(LeetCode 74题)
方法:将二维数组视为一维数组进行二分查找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int m = matrix.length;
int n = matrix[0].length;
int left = 0;
int right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
int midElement = matrix[mid / n][mid % n];
if (midElement == target) {
return true;
} else if (midElement < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
}
关键点:
- 将二维坐标转换为一维索引:
index = row * n + col - 将一维索引转换为二维坐标:
row = index / n,col = index % n
边界条件处理
- 空数组检查
- 单行或单列的特殊情况
- 目标值小于最小值或大于最大值的情况