简介:《Java学习路线》PDF 面向Java初学者及希望系统进阶的后端开发者,用一条从语言基础到工程落地的路径解决知识点零散、学习顺序不清的问题。内容按Java语法与面向对象、JVM与GC调优、并发编程、数据结构与算法、计算机网络、数据库与SQL、Web开发、Spring生态、微服务、前端基础、研发工具、Linux、设计模式等模块展开,并延伸到任务调度、链路追踪、ELK日志分析及Docker/Kubernetes容器化,形成较完整的后端知识图谱。资源包为1个PDF文件,大小约92KB,轻量便于随时查阅,适合按模块制定计划或在项目中查漏补缺。已有492人学习,可帮助读者理清从入门到后端开发的能力层级、技术栈关联与进阶方向,并结合实际项目巩固重点知识。
1. 一张后端知识地图:Java 学习路线为什么按「层」切而不是按「书」切
多数人的 Java 学习路线是从一本语法书起步的,把集合和 IO 啃完就以为地基打完了,真进项目才发现还要写 SQL、配 Nginx、解释为什么加了索引还是慢。这份路线图真正的价值不在技术名词的数量,而在它按调用链分层:语言与运行时在最底下,框架与中间件居中,分布式与工程化在最上面,算法、网络、操作系统像钢筋一样贯穿三层。按层复习的好处是每一层都能回答“上一层凭什么调用我”,而不是把 JVM、Spring、消息队列背成三座互不相连的孤岛。已经在写业务代码的人可以拿它当查漏清单,准备面试的人可以拿它当追问路径。
2. 语言与运行时底座:语法、JVM、并发与算法这四块怎么串
这一层是整张图里最容易被低估的部分。语法书翻完只需两周,但容器源码、JVM 内存布局、并发可见性这些问题,往往要花掉几个月才能真正讲明白。梳理下来会发现,这一层的知识点不是并列关系,而是层层递进:语法决定你能不能写出来,容器和泛型决定代码质量,JVM 决定你知道程序在机器上怎么跑,并发决定你在多核场景下会不会写出看起来对、跑起来错的代码。算法与网络、操作系统则是面试与排查问题的公共底座。
2.1 语言基本功:语法之后真正拉开差距的是容器、泛型与异常
基础语法、面向对象、接口这几块几乎没有复习价值,会就是会,不会也写不出能跑的代码。真正需要反复过的是容器体系:List、Map、Set 三大家族的实现差异决定了你在十万级数据量下是毫秒还是秒级响应。最典型的对比是 ArrayList 与 LinkedList,很多人背了“链表插入快”就无脑选后者,实际上头部插入之外,LinkedList 的随机访问和缓存局部性都更差,绝大多数业务场景 ArrayList 反而更稳。
| 容器 | 底层结构 | 典型适用场景 | 复习时要能讲清的点 |
|---|---|---|---|
| ArrayList | 动态数组 | 读多写少、按下标访问 | 扩容倍数、1.5 倍与拷贝成本 |
| LinkedList | 双向链表 | 频繁头尾增删、队列语义 | 为何实际项目里用得少 |
| HashMap | 数组 + 链表 + 红黑树 | 通用键值存储 | 负载因子 0.75、树化阈值 8 与退化阈值 6 |
| TreeMap | 红黑树 | 需要有序遍历或范围查询 | 与 LinkedHashMap 的顺序语义区别 |
| ConcurrentHashMap | 分段思想演进到 CAS + synchronized | 多线程共享缓存 | 1.8 之后为何放弃 Segment |
泛型和反射是另一个分水岭。泛型擦除意味着运行时拿不到List<String>里的 String,这直接解释了为什么不能new T[]、为什么重载f(List<String>)和f(List<Integer>)会编译失败。反射和注解则构成了所有框架的地基:Spring 扫描注解、MyBatis 映射字段、JSON 序列化取属性,全都走的是反射这条链,理解了Class、Method、Field三件套,看框架源码才不至于全程懵。
异常处理要分清 checked 与 unchecked 的边界:业务可恢复的用受检异常,编程错误用运行时异常,吞异常和catch (Exception e) {}是代码评审里的高频扣分项。I/O 部分重点不在会写FileInputStream,而在理解 BIO、NIO、多路复用的模型差异,这块和后面 Netty、Tomcat 的连接器设计是同一套知识。
2.2 JVM 与并发:能画出来才算真会,背参数是最低配
JVM 的复习顺序建议是:类加载机制 → 运行时数据区 → 垃圾回收 → 性能监控与调优。类加载五个阶段中,验证、准备、解析容易被跳过,但“准备阶段给静态变量赋零值、初始化阶段才赋真实值”这个细节,是无数道面试题的源头。双亲委派模型要能说清它解决了什么问题(同一个类不被重复加载、核心类库不被篡改),以及为什么 Tomcat、热部署这些场景要打破它。
运行时数据区里,堆和虚拟机栈的区别几乎人人会背,真正的分水岭是能不能说清一个方法调用发生时栈帧里发生了什么、对象在哪一刻从新生代进入老年代、方法区在 1.8 之后为什么换成了元空间。垃圾回收部分不要只记“标记清除、复制、标记整理”三个词,要能把分代假设和收集器对应起来:新生代对象朝生夕灭所以用复制算法,老年代存活率高所以用标记整理,CMS 追求低停顿但会产生碎片,G1 用 Region 划分把停顿做成可预测的。
提示:调优题的答题顺序永远是“先定位问题再改参数”,直接报
-Xmx数值的答案基本会被追问穿。
工具层面,jps看进程、jstat -gc看回收频率和耗时、jmap导出堆快照、jstack抓线程栈,这四个足够应付绝大多数线上排查。调优之前先问三个问题:是 GC 频繁还是单次停顿长?是内存泄漏还是内存不足?是锁竞争还是 CPU 打满?答案不同,改的参数完全不同。
并发部分的知识密度最高。JMM 定义了主内存与工作内存的抽象,volatile保证可见性和禁止指令重排但不保证原子性,synchronized保证原子性、可见性和有序性。happens-before 规则是判断“这段代码在多线程下有没有问题”的推理工具,比背结论可靠得多。锁升级路径(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)要能对应到具体的竞争场景,AQS 的 state 与 CLH 队列则是理解ReentrantLock、CountDownLatch、Semaphore的统一入口。
线程池是这一节最该动手写一遍的东西,七个参数少一个都答不完整:
// 手动创建线程池,避免 Executors 工厂方法的无界队列风险 ThreadPoolExecutor pool = new ThreadPoolExecutor( 4, // corePoolSize:常驻核心线程数 8, // maximumPoolSize:峰值可扩到多少 60L, TimeUnit.SECONDS, // 非核心线程空闲回收时间 new ArrayBlockingQueue<>(200), // 有界队列,防止任务无限堆积 new ThreadFactory() { // 自定义线程名,方便 jstack 定位 private int i = 0; public Thread newThread(Runnable r) { return new Thread(r, "biz-pool-" + i++); } }, new ThreadPoolExecutor.CallerRunsPolicy() // 满了之后回退给调用线程 );这段配置的逻辑是:核心 4 线程先顶着,队列积压到 200 之后再扩到 8 线程,还满就触发拒绝策略。参数怎么改取决于任务类型:CPU 密集型大致设成核数加一,IO 密集型可以放大到核数的数倍,但前提是队列必须有界,Executors.newFixedThreadPool的无界队列在流量突增时会把内存吃干净,这是血泪经验级别的坑。四种拒绝策略里,CallerRunsPolicy适合能容忍降速但不能丢任务的场景,AbortPolicy适合必须快速失败的场景。
2.3 算法、网络与操作系统:被当成公共课,其实是排查问题的底层
数据结构与算法的复习要区分两条线:一条是面试线,重点是复杂度分析、二分边界、链表操作、二叉树遍历、动态规划的状态定义;另一条是工程线,重点是能不能判断出这段循环是 O(n²) 从而在数据量上来前改掉。排序算法不必默写全部,但要能说清快排为什么平均 O(n log n) 而最坏退化到 O(n²)、归并为什么稳定、堆排序为什么适合求 TopK。
| 算法思想 | 典型题型 | 复习重点 |
|---|---|---|
| 二分查找 | 有序数组查找、边界问题 | 左右区间的开闭写法必须统一 |
| 双指针 | 链表、数组去重 | 指针移动条件与终止判断 |
| 动态规划 | 背包、最长子序列 | 状态定义与转移方程,不是背题 |
| 回溯 | 排列组合、棋盘类 | 剪枝条件决定会不会超时 |
| 贪心 | 区间调度 | 能否证明局部最优推出全局最优 |
计算机网络的重点是 TCP 与 HTTP 两条线。三次握手要能解释“为什么不是两次”,四次挥手要能解释 TIME_WAIT 存在的意义,HTTP 要能从报文结构讲到 1.1 的持久连接、2 的多路复用、HTTPS 的握手与证书校验。Cookie 与 Session 的区别背后是“状态放在客户端还是服务端”的架构选择,再往后延伸到 Token 与无状态鉴权,这条线索能一直连到微服务的网关鉴权。
操作系统部分不用啃教材,抓住进程与线程的区别、上下文切换的成本、死锁的四个必要条件、I/O 模型演进这几块就够了,它们和 JVM 线程模型、Netty 的 Reactor 模式是同一套语言。
3. 框架与中间件层:Spring、缓存、消息队列与数据访问的分工
这一层的复习目标是建立“谁负责解决什么问题”的直觉。很多人能说出 Spring 有 IoC 和 AOP,却说不出一个请求从进网关到落库到底经过了哪些组件;能背出 Redis 五种数据类型,却说不清什么场景该用它而不是本地缓存。梳理下来,这一层的所有技术都在回答四个问题:对象谁来管、数据放哪里、服务之间怎么通信、数据库压力怎么卸。
3.1 Spring 家族:一条从 Bean 容器到自动配置的完整链
Spring 的核心只有两件事:IoC 容器负责对象的生命周期和依赖注入,AOP 负责把日志、事务、权限这类横切逻辑从业务代码里抽出来。Bean 的生命周期是面试高频区,要能按顺序说出实例化、属性填充、BeanPostProcessor前置处理、初始化方法、后置处理、使用、销毁这几步,并且知道@PostConstruct和InitializingBean在链条里的位置。
循环依赖是另一个必考点。三级缓存的设计意图是:一级放成品 Bean,二级放半成品,三级放能生成半成品的工厂,从而在 A 依赖 B、B 又依赖 A 时,让 B 拿到 A 的早期引用。但要注意,构造器注入的循环依赖三级缓存也救不了,@Async这类需要代理的场景也可能绕不过去,这解释了为什么官方更推荐构造器注入加设计上的解耦。
Spring MVC 要抓住 DispatcherServlet 这一个入口:请求进来先过 HandlerMapping 找处理器,再过 HandlerAdapter 执行,返回值经 ViewResolver 或消息转换器输出。把这条链画一遍,拦截器、参数绑定、全局异常处理各自插在哪一步就都清楚了。
Spring Boot 的自动配置是这层的价值高点。@SpringBootApplication拆开是三个注解,其中@EnableAutoConfiguration通过AutoConfiguration.imports(新版本)或spring.factories(旧版本)加载候选配置类,再用@ConditionalOnClass、@ConditionalOnMissingBean这类条件注解决定要不要生效。理解了这一层,你就能回答“为什么引了 starter 就有默认数据源,而我定义了一个 DataSource 之后默认的就不生效了”。
// 自定义一个 starter 的核心配置类,演示条件装配的思路 @Configuration @ConditionalOnClass(name = "com.example.demo.SomeClient") // 类路径存在才生效 public class DemoAutoConfiguration { @Bean @ConditionalOnMissingBean // 用户自己定义了就不再覆盖 public SomeClient someClient(DemoProperties props) { // props 绑定 application.yml 里 demo.* 开头的配置 return new SomeClient(props.getEndpoint(), props.getTimeout()); } }这段代码的关键在条件注解的顺序:先判断依赖是否存在,再判断用户是否已经自定义。整条自动配置链的设计哲学是“约定优于配置,但任何时候用户的手动配置优先级更高”,这也是排查“我的配置为什么不生效”时的第一直觉来源。
事务是 Spring 里最容易出错的部分。@Transactional的传播行为有七种,常用的只有REQUIRED(有则加入、无则新建)和REQUIRES_NEW(挂起当前、独立新建)两种。要记住几个失效场景:同类内部方法直接调用不走代理、异常被 catch 掉不回滚、非 public 方法在默认代理模式下不生效、以及只对运行时异常回滚而受检异常默认不回滚。这几个点比背传播行为表实用得多。
权限框架方面,Shiro 轻量、上手快,适合单体应用;Spring Security 与 Spring 生态结合更紧,适合需要 OAuth2、SSO 的场景。复习时不必两边都钻,抓住认证与授权分离、过滤器链的执行顺序这两条主干即可。
3.2 缓存与消息队列:先想清楚解决什么,再挑组件
缓存的第一课不是 Redis 命令,而是“缓存放哪一层”。本地缓存(如基于 Caffeine 的方案)纳秒级、无网络开销,但多实例之间不一致;分布式缓存(Redis、Memcached)一致性好,但多一跳网络。常见做法是本地缓存扛热点、分布式缓存兜底,两级缓存之间用失效通知同步。
Redis 的复习顺序建议是:五种数据类型的适用场景 → 过期与淘汰策略 → 持久化 → 高可用 → 常见问题。String 适合计数和序列化对象,Hash 适合存对象的部分字段更新,List 适合简单队列,Set 适合去重和交并集,ZSet 适合排行榜和延时队列。持久化要能说清 RDB 是快照、AOF 是追加日志、混合持久化怎么折中。缓存穿透、击穿、雪崩三个词的应对手段要能一一对应:布隆过滤器或空值缓存防穿透,互斥锁或逻辑过期防击穿,随机过期时间加多级缓存防雪崩。
注意:分布式锁的可靠性依赖“加锁与过期必须原子”,手写
setnx再expire中间会留出窗口,用一条带 NX 和 EX 参数的命令才能闭环。
消息队列的价值只有三个词:解耦、异步、削峰。所有 MQ 的选型讨论都围绕这三点展开。RabbitMQ 路由灵活、延迟低,适合业务解耦和复杂路由;Kafka 吞吐极高、天然支持分区顺序,适合日志与埋点这类大数据量场景;RocketMQ 在事务消息、顺序消息、延时消息上支持较完整,适合订单、支付这类对可靠性要求高的链路。
消息可靠性要按“生产端、存储端、消费端”三段拆:生产端用发送确认机制防止丢消息,存储端靠刷盘策略和副本保证不丢,消费端用 ACK 加手动确认保证处理完再提交位点。但只要引入重试,幂等就成了必答题:消息带唯一业务 ID,消费端用去重表或状态机判断是否已处理。顺序消息则要保证同一业务键路由到同一分区,且消费端单线程处理。
3.3 数据访问与 RPC:ORM、连接池、分库分表和远程调用
数据访问层的复习要从 SQL 往上走,而不是从框架往下走。SQL 部分的核心是索引:B+ 树为什么适合做索引、聚簇索引与二级索引的区别、回表是什么、最左前缀原则为什么成立、覆盖索引怎么用。explain要能看懂type、key、rows、Extra四个字段,看到Using filesort和Using temporary就知道该优化了。
-- 联合索引 (user_id, status, created_at) 的典型用法 EXPLAIN SELECT id, amount FROM orders WHERE user_id = 10086 -- 命中索引第一列 AND status = 1 -- 命中第二列 AND created_at > '2024-01-01' -- 第三列用于范围,之后的条件不再走索引 ORDER BY created_at DESC;这段查询能走索引的原因是条件顺序与索引列顺序一致,且id、amount若在索引里就能避免回表。最容易踩的坑是范围查询之后的列失效,以及status这种区分度极低的列单独建索引几乎没有收益。
事务与隔离级别是数据库部分的深水区。四种隔离级别要能对应到具体现象:读未提交会脏读,读已提交会不可重复读,可重复读在标准定义下仍可能幻读,串行化全锁上但性能最差。MySQL 的默认级别是可重复读,并通过 MVCC 加间隙锁在很大程度上规避了幻读,这个“标准定义”与“具体实现”的差异是最容易被追问的地方。
ORM 框架方面,MyBatis 胜在 SQL 可控、易于优化,JPA 和 Hibernate 胜在对象映射和跨库移植。连接池选型上,Druid 带监控、HikariCP 性能好,选哪个不如搞清楚“连接池大小怎么定”这个真问题:连接数不是越大越好,超过数据库承载能力后只会互相争抢。分库分表属于“不到万不得已不用”的手段,真要用,先想清楚分片键怎么选、跨分片查询和聚合怎么处理、扩容时数据怎么迁移,分片中间件(如基于代理的 MyCat 与基于客户端的 ShardingSphere)只是把这些问题换了个地方暴露。
RPC 框架的复习主线是:序列化协议、通信框架、服务注册发现、负载均衡、容错。Dubbo 在国内使用广,gRPC 依托 HTTP/2 和 Protobuf 适合多语言场景,Thrift 胜在跨语言代码生成,而 Netty 是这些框架底下的通信底座,理解它的 Reactor 线程模型和零拷贝机制,看任何 RPC 框架的源码都能对上号。
4. 分布式与工程化:微服务组件、运维体系与工具链
到了这一层,知识点从“怎么写代码”变成“怎么让一堆服务稳定地跑起来”。这一层的复习难点不在名词多,而在于每个组件都要能回答“去掉它行不行、换一个行不行”。梳理下来,微服务的所有组件都在解决两类问题:服务之间怎么找到彼此并可靠通信,以及出了问题怎么快速定位和止损。
4.1 微服务六件套:注册发现、网关、负载均衡、熔断、配置与分布式事务
服务注册与发现是微服务的第一块砖。Eureka 偏向 AP,节点间异步复制,网络分区时仍能提供服务但可能读到旧数据;Zookeeper 偏向 CP,选举期间不可用;Consul 与 Nacos 都支持两种模式切换,Nacos 还额外提供配置中心能力。选型的关键不是谁更先进,而是业务能否接受短暂的注册表不一致。
网关是所有流量的统一入口,承担路由、鉴权、限流、日志聚合四件事。Zuul 1 基于阻塞模型,Spring Cloud Gateway 基于响应式模型,在高并发下后者更有优势。网关鉴权的典型做法是在网关统一校验 Token 并把用户信息透传到下游,避免每个服务重复实现。
负载均衡分客户端和服务端两种。Ribbon 属于客户端负载均衡,调用方自己持有服务列表并选择实例,优点是少一跳、缺点是列表更新有延迟;服务端负载均衡由中间代理完成,省心但多一次转发。轮询、加权、最少连接、一致性哈希几种策略的差异要能对应到场景:会话粘性用一致性哈希,机器性能不均用加权。
熔断与降级解决的是雪崩问题。Hystrix 的隔离方式有线程池隔离和信号量隔离两种,前者彻底但有线程切换开销,后者轻量但无法真正超时中断。核心参数有三个:超时时间、熔断窗口内的失败率阈值、半开状态的试探请求数。很多团队配了这些参数却没验证过链路,结果真出故障时熔断没触发,这是典型的“配置了等于没配”。
配置中心的价值在于“改配置不发版”。Config、Apollo、Nacos 三者的差异主要在推拉模式、灰度能力、权限模型上。复习时要能说清一次配置变更的生效路径:改配置 → 发布 → 客户端监听或轮询 → 本地刷新 → 是否需要重启。
分布式事务是最容易背成名词表的一节。2PC 与 3PC 是强一致路线,靠协调者两阶段提交,代价是同步阻塞和协调者单点;XA 是 2PC 的标准化实现;TCC 是业务层的补偿模式,需要写 Try、Confirm、Cancel 三个方法,侵入性强但性能好;SAGA 把长事务拆成一系列本地事务加补偿;本地消息表靠可靠消息加消费端幂等实现最终一致;Seata 的 AT 模式通过解析 SQL 生成反向快照来简化补偿逻辑。真正要理解的是:不要问哪个方案最好,要问这条业务链路能不能接受最终一致、补偿失败的兜底由谁来做。
4.2 服务器与运维:Nginx、Tomcat、CI/CD、日志与链路追踪
Nginx 的复习重点是反向代理、负载均衡、静态资源服务和限流。最基本的配置要能看懂 upstream 块和 location 匹配优先级,proxy_pass结尾带不带斜杠会改变路径拼接结果,这是部署时最常见的 404 来源。
# nginx.conf 片段:把 /api/ 前缀转发到后端集群,并做简单的连接数限流 upstream backend_pool { least_conn; # 最少连接策略,适合请求耗时差异大的场景 server 127.0.0.1:8081 weight=2; # 权重高,承接更多流量 server 127.0.0.1:8082; } limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s; location /api/ { limit_req zone=api_limit burst=40 nodelay; # 允许突发 40 个,其余排队或拒绝 proxy_pass http://backend_pool; # 注意此处不带路径,会保留 /api/ 前缀 proxy_set_header X-Real-IP $remote_addr; # 透传真实客户端 IP }这段配置的意图是:上游按权重分配流量,least_conn让慢请求少的机器多接活;限流按 IP 每秒 20 个请求,突发允许 40 个。实际部署时最容易踩的坑是proxy_pass的斜杠语义和location的匹配优先级(精确匹配优先于前缀匹配,正则匹配又有自己的顺序),配置改完必须用nginx -t验证。
Tomcat、Jetty、Undertow 三者的差异在连接器模型和线程模型上,复习时抓住“连接数、工作线程数、最大排队数”三个参数,以及“为什么高并发下 Tomcat 的阻塞 IO 会拖垮线程池”这个核心问题,就能理解 Netty 这类 NIO 框架为什么存在。
持续集成与持续发布是工程化的关键能力。Jenkins 流水线的典型阶段是拉代码、构建、单测、代码质量检查、打镜像、部署。代码质量检查重点看重复率、圈复杂度和阻断级别的静态缺陷,不要一上来就开全量规则,否则老项目会永远红着。日志收集走采集、缓冲、存储、检索四段,链路追踪则靠 TraceID 串联,核心是“一次请求的上下文如何跨线程和跨服务传递”。
4.3 研发工具与前端:Git、Maven、Docker、K8s 与前端三大件
工具链部分经常被当成“用熟就行”,但真出问题时全靠这些细节救场。Git 要能说清工作区、暂存区、本地仓库的关系,merge与rebase的取舍(分支历史清晰度 vs 提交记录线性),以及reset三种模式的区别。团队协作中最容易翻车的是“合并了别人的强制推送”和“误提交敏感配置”,前者靠保护分支规避,后者靠提交前钩子和gitignore兜底。
Maven 的复习点是生命周期、依赖调解和私服。依赖冲突有两套原则:最短路径优先和第一声明优先,冲突排查靠mvn dependency:tree。多模块项目的依赖管理要靠父 POM 的dependencyManagement统一版本,避免子模块各写各的版本号。
容器化是这一层必须动手的部分。Docker 的镜像分层机制决定了 Dockerfile 的写法:把变化频率低的指令放前面,把拷贝代码放后面,才能吃到构建缓存。多阶段构建可以把编译环境和运行环境分开,最终镜像只留运行时产物。
# 多阶段构建:编译阶段用带完整 JDK 的镜像,运行阶段只留精简版 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /src COPY pom.xml . RUN mvn -q dependency:go-offline # 先拉依赖,命中缓存 COPY src ./src RUN mvn -q package -DskipTests FROM eclipse-temurin:17-jre # 运行时镜像,不含编译工具 WORKDIR /app COPY --from=build /src/target/app.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]这段写法的关键在于前后两段分离:依赖下载单独成层,源码修改不会导致依赖重新下载;运行镜像不带 Maven 和编译器,体积和攻击面都更小。Kubernetes 则要抓住 Pod、Deployment、Service、Ingress 四个基本对象的关系,理解“声明式”与“自愈”的含义:你描述期望状态,控制器负责把实际状态拉回来。
前端部分不必深入,但三大件(HTML、JavaScript、CSS)至少要能改页面、调接口、看控制台报错。Ajax 与 fetch 的区别、跨域的本质(同源策略加浏览器拦截)、模板引擎与前后端分离的演进,这几条理解了,和后端联调时就不会把所有问题都推给对方。
5. 易漏易错点:五个“以为自己会了”的高频坑
每个学完路线图的人都会产生一种错觉:名词都认识,就等于掌握了。实际上,真正区分水平的是那些“能讲清细节”的地方。梳理下来,下面五个坑几乎人人都踩过,共同的成因是学习时停留在“名词层”,没有下沉到“机制层”。
5.1 并发:会背 synchronized,说不清 happens-before
易漏点:能写出synchronized和volatile的用法,但被问到“两个线程先后写同一个变量,另一个线程何时能看到”时答不上来。
为什么漏:JMM 是抽象模型,没有对应的代码形态,看书时容易被当成一段描述性文字跳过。而可见性问题在单机小流量下几乎不复现,出问题时又表现为玄学般的偶发。
怎么补:把 happens-before 的几条规则(程序顺序、监视器锁、volatile 变量、线程启动与终止、传递性)抄在一张纸上,然后拿三个真实场景去套:双重检查锁为什么要给实例加volatile、final域为什么能保证构造完成后的可见性、线程池提交任务前的写入为什么对工作线程可见。套不通就回去重读,直到每条规则都能找到一个真实代码片段。
5.2 JVM:参数背了一堆,不会读 GC 日志
易漏点:能说出-Xms、-Xmx、-Xmn、-XX:MetaspaceSize的含义,但给一份 GC 日志不知道从哪看起。
为什么漏:调优场景在真实项目里出现频率低,大多数人没有亲手制造过一次内存溢出,自然没有看过日志。
怎么补:本地起一个会持续创建对象的 Demo,把堆设小一点,用-Xlog:gc*(或旧版本的-XX:+PrintGCDetails)打印日志,然后加jmap -histo:live看对象分布,再用jmap -dump导出快照,最后用工具打开看引用链。这一套走完一遍,你会突然明白“内存泄漏”和“内存不足”在日志里的表现完全不同。整个过程不追求记住每个参数,追求的是排查动作形成肌肉记忆。
5.3 数据库:隔离级别倒背如流,不知道 MySQL 的 RR 是怎么防幻读的
易漏点:四种隔离级别和对应现象能默写,但被追问“MySQL 默认 RR 为什么在标准里允许幻读、它又怎么规避的”就卡住。
为什么漏:教材讲的是 SQL 标准,而实际实现是另一套逻辑,两者的差异不在课本的同一页上。
怎么补:把 MVCC 的 ReadView、undo log 版本链、当前读与快照读的区别串起来看一遍,然后自己开两个会话做实验:会话 A 开启事务查询一次,会话 B 插入并提交,会话 A 再查一次看结果,接着在会话 A 里执行select ... for update再看一次。实验比背书有效十倍,因为你能亲眼看到快照读和当前读返回不同结果。间隙锁的作用也要在这个实验里一并体会。
5.4 Spring:循环依赖能说出三级缓存,说不清 AOP 为什么在自调用时失效
易漏点:知道 AOP 基于动态代理,但写了一个同类内部方法互调的代码,发现事务注解没生效,不知道原因。
为什么漏:代理机制是“容器替你做的事”,写业务代码时看不见,所以从来不会想到去验证。
怎么补:先明确一个判断标准——只有通过代理对象调用才会走增强逻辑,this.method()走的是原始对象。然后在同一个类里写一个方法调用带事务注解的方法,分别在同类内调用和从外部注入调用两种方式下观察结果,差异会非常直观。真要解决,无非三种做法:拆到另一个 Bean、注入自身代理、用AopContext配合暴露代理。理解了这个,@Async、@Cacheable失效的问题也就一并解决了。
5.5 分布式事务:方案名字全知道,讲不清一条下单链路的事务边界
易漏点:能列出 2PC、TCC、SAGA、本地消息表、AT 各自的原理,但给一条包含下单、扣库存、发优惠券、发通知的链路,说不清该在哪切分事务。
为什么漏:分布式事务本质是业务建模问题,不是技术选型问题,而大多数资料只讲方案不讲建模。
怎么补:拿一条真实的业务链路,把每个步骤标注三个属性:是否强一致、能否补偿、失败后谁兜底。你会发现大部分步骤其实只需要最终一致,真正需要强一致的可能只有“扣库存与创建订单”这一小段。标完之后再选方案,通常比先选方案再硬套要省事得多。同时要盯住三个工程细节:幂等键怎么设计、补偿失败后进哪张表、人工介入的入口在哪。
6. 查漏补缺:把路线图变成一张能打勾的复习清单
复习到最后阶段,最有效的方式不是再看一遍资料,而是拿一张清单逐项自检。自检的颗粒度决定效果:只写“JVM”三个字的清单等于没写,必须写到“能用 jstat 判断是 Young GC 频繁还是 Full GC 频繁”这种可验证的动作级别。
我习惯把每一项分成三个层级:能讲清(能向别人解释原理)、能写出(能手写关键代码或命令)、能调通(在本地跑起来并看到结果)。三层都过的项可以直接划掉,只过前两层的要补动手,一层都没过的回炉。下面这张表是我自己用的分组样例,可以直接照着扩成完整版。
| 模块 | 自检动作 | 达标标准 |
|---|---|---|
| 容器 | 手写 HashMap 的 put 与扩容逻辑 | 能解释树化阈值与扩容时机 |
| 并发 | 手写一个有界线程池并压测 | 能说明拒绝策略触发条件 |
| JVM | 用 jmap 导出快照并定位大对象 | 能说出对象为何晋升老年代 |
| 数据库 | 用 explain 优化一条慢查询 | 能判断回表与索引失效原因 |
| Spring | 复现一次事务失效并修复 | 能解释代理与自调用的关系 |
| 微服务 | 画一次请求经过网关到落库的全链路 | 能标出每个组件的失败点 |
| 工程化 | 写一条从提交到部署的流水线 | 能说明每个阶段的失败处理 |
执行这张表时有两个经验值得记住。第一个是限时:每个自检动作给 40 分钟,超时就标记为“不熟”,不要在一个点上耗一整天,那样复习节奏会崩。第二个是留痕:每个动作写三行笔记,一行写结论,一行写踩到的坑,一行写下次怎么快速回忆。攒到几十条之后,这份笔记本身就是最好的考前材料。
# 复习环境自检脚本示例:一条条确认工具链是否可用 java -version 2>&1 | head -n 1 # 确认 JDK 版本与路线图一致 mvn -v | head -n 1 # 确认构建工具可用 docker ps >/dev/null && echo "docker ok" # 容器环境是否就绪 jps # 能列出 Java 进程,才谈得上后续排查这个脚本的意义不在命令本身,而在建立“环境先于知识”的意识:很多次复习卡住,其实是 JDK 版本不对、Maven 拉不到依赖、Docker 没启动这类环境问题,把时间浪费在了错误的方向上。
从那以后我每次复习任何一块新内容,都会强制走一遍“讲一遍、写一遍、跑一遍”的流程,哪怕是最熟的集合和线程池也不例外,因为翻车往往就发生在自以为最熟的地方。希望帮到你。
本文还有配套的精品资源,点击获取