今天是我java求职学习day51的第51天。说实话,能坚持到这个数字的人并不多。身边同期备考的朋友,有的在第二周就转投考研,有的在第四周陷入"越学越焦虑"的循环,真正按计划推进到这里的,一只手数得过来。
Day51是一个很微妙的时间节点。基础语法早过完了,SpringBoot项目能撸能跑,力扣也刷了一百多道,但心里清楚离"从容进面试"还有一段距离。这个阶段的典型特征是——已经过了学新东西的兴奋期,进入"查漏补缺"的深水区,每天拼的不是冲劲,而是稳定的输出。
今天我完整记录了这一天的安排:上午深挖并发编程的高频考点,下午做了一场模拟面试,晚上刷两道算法题,再重新打磨简历里项目的讲述逻辑。这篇文章既是复盘,也是写给同样在求职路上的人的一份参考——如果你也走到第40到60天这个区间,这些内容大概率用得上。
1. 第51天的坐标:你的知识地图该铺到什么位置
1.1 回看前50天,一份可复制的节奏
先把前50天的节奏摊开讲。这个阶段能不能走得稳,很大程度上取决于前期知识地图铺得是否均匀。我把自己50天的安排复盘了一遍,大致是这个结构:
| 时间段 | 主线任务 | 核心产出 |
|---|---|---|
| Day1-10 | Java基础语法、集合源码、IO、泛型与反射 | 集合类源码能讲出扩容机制,Map的哈希冲突处理随口能答 |
| Day11-20 | JVM内存模型、GC算法与调优、类加载、并发基础 | 能画出堆内部分区,说清对象晋升流程 |
| Day21-30 | Spring/SpringBoot原理、MyBatis、MySQL索引与事务隔离 | 能徒手搭接口,对慢SQL有优化思路 |
| Day31-40 | Redis核心数据结构、缓存一致性、消息队列、分布式基础 | 能聊缓存穿透与雪崩,能画出分布式锁的时序 |
| Day41-50 | 项目打磨、LeetCode刷到100道、第一次模拟面试 | 简历定稿,开始熟悉面试的追问节奏 |
这个安排有一个核心原则:每十天一个主题,主题内只做"主线加配套"两件事。主线是某一类技术深度,配套是算法题的持续跟进。前期容易犯的错是东一榔头西一棒子,今天看并发,明天看Spring,后天又去刷MySQL,看起来每天都很忙,最后什么都是半吊子。我的建议是把技术栈按面试权重排好序,一个块一个块地啃透,再进入下一块。
举个例子,我当时在Day12到Day18专门啃JVM,每天只干两件事:读一段《深入理解Java虚拟机》的章节,再用jvisualvm连上一个本地项目观察堆和GC的实际情况。刚开始觉得慢,但到了Day20左右,遇到面试题"说说对象什么时候进入老年代",我能直接背出年龄阈值、动态年龄判定、大对象直接进入老年代这三个维度,这就是模块化学习的回报。
1.2 深水区状态判断:你是在复习,还是在假装努力
到了第51天,最怕的不是不会,而是用"新东西很多"来掩盖"旧东西没消化"。我自己有一个很简单的判断标准:能不能在不看笔记的情况下,把某个知识点讲给一个不懂技术的人听。如果讲得出来对方也能听懂,说明真的理解了;如果讲的时候要"嗯……那个……",说明还处于记忆层面。
费曼学习法在这个阶段非常管用。每天抽出半小时,随便挑一个今天复习过的概念,录一段两分钟的语音讲给自己听,回放的时候会发现很多自以为懂、其实含混的地方。我Day48那天录了"MySQL的MVCC机制",回放才发现自己连"当前读和快照读的区别"都说不利索,这个短板如果不暴露,面试现场大概率也会卡壳。Day51这种深水区,拼的就是谁能先意识到"我知道自己不知道什么"。
另外一个状态判断是做题正确率。如果力扣简单题还要想半天,或者高频题看到题号能说出解法但代码写不利索,那就说明前期的算法手感还没沉淀下来。这个阶段不需要再开新专题,把错题反复刷,比刷新题重要得多。我自己有一个硬性指标:一道做过标记的错题,隔五天重新做,如果能独立在20分钟内写对且跑通测试用例,才算真正过关,否则继续进回收站。
2. 今日主攻:并发编程高频考点,逐个过成肌肉记忆
2.1 synchronized和ReentrantLock,别再停留在背八股
上午第一个专题是锁。很多面试一开口就是"synchronized是重量级锁,ReentrantLock更灵活",这种回答只能证明你背过八股,证明不了你理解。真正需要讲清楚的,是它们各自的实现机理和取舍。
synchronized是JVM指令级的锁,编译后对应monitorenter和monitorexit指令,底层依赖Monitor对象。JDK1.6之后引入锁升级机制:偏向锁、轻量级锁、重量级锁。锁升级的目的很直接——大多数场景下并发竞争并不激烈,没必要一上来就上重量级锁,能用偏向锁解决的就不升级。这里有一个面试官常问的细节:偏向锁在什么条件下会撤销?答案是当有另一个线程尝试竞争该锁时,偏向锁会撤销并升级为轻量级锁;如果竞争更激烈,轻量级锁自旋失败后会膨胀为重量级锁。
ReentrantLock是JDK层面的API,基于AQS实现。它和synchronized的核心差异,我从面试角度整理了一张表:
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM指令级,Monitor机制 | JDK API,基于AQS与LockSupport |
| 公平性 | 只支持非公平 | 可选公平与非公平 |
| 锁超时 | 不支持 | tryLock(timeout, unit)可设置等待上限 |
| 中断响应 | 不响应中断 | lockInterruptibly()可被中断 |
| 条件队列 | 只有一个隐式等待集 | 可通过newCondition()创建多个条件队列 |
| 锁升级 | 有偏向锁到重量锁的升级路径 | 无升级概念,核心是state状态维护 |
这里再说一个容易被问倒的点:既然synchronized有锁升级机制,性能已经不输ReentrantLock,为什么很多框架还是偏好ReentrantLock?因为ReentrantLock的灵活度是synchronized给不了的。典型场景是tryLock带超时获取锁,拿不到就做降级处理,避免线程无限期阻塞;还有多个条件队列的场景,典型的是生产者-消费者模型里区分"缓冲区满"和"缓冲区空"两种等待条件。这些需求用synchronized实现起来非常别扭。
再补充一个容易被忽视的细节:synchronized在JDK14以后引入了虚拟线程相关的改进,但在面试中聊到这个层面的人很少,我建议你不要主动往这个方向引,除非面试官自己提。多数情况下,把锁升级机制和ReentrantLock的条件队列讲透就足够建立区分度了。
2.2 AQS源码怎么读,从acquire到队列唤醒
ReentrantLock的核心在AQS(AbstractQueuedSynchronizer)。Day51这个阶段,源码不能只看结论,至少要能画清调用链。我建议按这个顺序读:先看ReentrantLock的lock()入口,然后进到Sync的lock(),再到AbstractQueuedSynchronizer的acquire()。
acquire(int arg)的核心逻辑是三步:先tryAcquire尝试获取锁,如果失败就addWaiter把当前线程包装成Node节点放进CLH队列尾部,然后acquireQueued在队列里自旋或挂起,等待前驱节点释放锁后唤醒自己。源码核心可以抽象成下面这个伪代码:
public final void acquire(int arg) { if (!tryAcquire(arg)) { Node node = addWaiter(Node.EXCLUSIVE); // 入队 boolean interrupted = acquireQueued(node, arg); // 自旋或阻塞 if (interrupted) { Thread.currentThread().interrupt(); } } }看这段代码有两个关键点。第一,tryAcquire是一个由子类实现的模板方法,ReentrantLock的公平锁与非公平锁就是在这个方法里分叉的。非公平锁直接CAS设置state,谁抢到算谁;公平锁会先检查hasQueuedPredecessors(),如果队列里已经有等待线程,就乖乖排到后面,不参与竞争。第二,acquireQueued内部是一个for循环,循环体里先判断前驱节点是不是头节点,是的话再tryAcquire一次,成功就把自己设为头节点返回;否则用shouldParkAfterFailedAcquire判断前驱节点的waitStatus,需要挂起就调用LockSupport.park挂起当前线程。
我用一个生活类比帮助理解:AQS的队列就像餐厅门口排队叫号。每个新来的都要先取号(addWaiter),队伍里每个人都盯着前一个人(前驱节点),前一个人走了(释放锁)才会通知你。叫到号(tryAcquire成功)才能进去,等不及的人可以走(取消排队),但他前面的人并不知道,所以要靠waitStatus状态位去通知后一个节点。
读AQS不要一上来就啃全部代码。先读独占模式(ReentrantLock),再读共享模式(CountDownLatch、Semaphore),最后看读写锁ReentrantReadWriteLock在共享和独占之间切换的细节。每一步都要配合debug跟踪,打印线程名和队列变化,比自己盯着源码空想要高效得多。我当时在本地起了一个多线程的小demo,故意让几个线程争抢同一把锁,然后在intellij里设置条件断点看队列节点的变化,这样印象会比干读深好几倍。
2.3 线程池七个参数,以及面试官最爱追问的三种场景
线程池是并发高频考点中的高频。七个参数要能默写:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。
面试官不会满足于让你背参数。最经典的追问是:一个任务提交进来后,执行流程是怎样的?标准答案是:先判断核心线程是否满,没满就创建核心线程执行;满了就把任务放到工作队列;队列也满了,再创建非核心线程执行;线程数达到maximumPoolSize后,触发拒绝策略。这个顺序如果记混,后面所有延伸问题都会答歪。
第二个高频追问是:线程池大小怎么定?这个问题没有标准答案,但有通用的估算思路。CPU密集型任务,线程数建议设为CPU核数加1;IO密集型任务,可以设为CPU核数乘以2,或者用公式:CPU核数乘以(1加上等待时间除以计算时间)。实际项目中很少精确计算,通常是压测后调整。面试时能说出"我是这么定大小的:先按业务类型判断是CPU密集还是IO密集,给一个理论值,再压测调整",比直接报一个数字有价值得多。
这里顺便给一个本地压测的方法:用JDK自带的ThreadPoolExecutor写一个小程序,循环提交模拟任务,通过ThreadPoolExecutor的getPoolSize()、getQueue().size()、getActiveCount()等指标观察线程池状态变化,再配合JMC(JDK Mission Control)的线程诊断视图,基本能判断出参数是否合理。
第三个追问是:拒绝策略分别适合什么场景?这张表需要记清楚:
| 拒绝策略 | 行为表现 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛RejectedExecutionException | 默认策略,任务不能丢且要快速感知 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 不想丢任务,允许任务执行变慢 |
| DiscardPolicy | 静默丢弃任务 | 允许丢弃,对数据不敏感 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务 | 任务有有效期,如缓存刷新 |
这里我想强调一个容易被忽略的坑:自定义线程池时不要用Executors自带的便捷方法,尤其不要用newFixedThreadPool和newSingleThreadExecutor,它们内部用的都是无界队列LinkedBlockingQueue,任务堆积时不会有拒绝策略兜底,极端情况下内存会被撑爆。业界流传较广的Java开发规范里明确建议线程池要手动创建,显式传入有界队列和饱和策略。比如用ArrayBlockingQueue且容量设一个合理上限,配合CallerRunsPolicy,让生产者在队列满时自己承担执行压力,反而能形成天然的背压机制。
最后一个实用细节:核心线程默认不会预创建,要等任务提交才创建。如果希望系统启动时就准备好核心线程,可以调用prestartAllCoreThreads(),这在流量突刺明显的场景下很管用,能省去冷启动阶段的线程创建时间。另外线程池里的线程命名千万别用默认的pool-1-thread-1,排查问题时日志里根本分不清谁是谁,自定义一个ThreadFactory加上业务前缀,线上定位问题会轻松很多。
3. 下午模拟面试:一场对话暴露三个真实短板
3.1 追问一:队列满了,新任务到底去哪
下午的模拟面试我拉了同为求职者的好友来当面试官,约定好只做技术追问,全程录音。第一道题就是线程池流程的连环追问。我答到"队列满了就创建非核心线程"的时候,对方突然追问:如果maximumPoolSize也满了,任务交给谁?我卡了两秒才想起拒绝策略,但只说出一个AbortPolicy,另外三个说不全。复盘时发现这次卡壳不是不会,而是平时没有把拒绝策略当成一个独立的知识块来组织,只是零散地记了几个名词。
正确回答的框架是先说流程,再说兜底:核心线程满到队列满到非核心线程满,最终触发拒绝策略;拒绝策略有四种,默认AbortPolicy抛异常,项目上我一般用CallerRunsPolicy做背压,或者自定义策略做降级记录。把这个流程练成肌肉记忆,再遇到任何延伸问题(比如队列用有界还是无界、为什么不用Executors默认线程池)就都能接住。
模拟面试的价值就在这里:平时看资料是线性输入,面试是跳跃式追问,两种思维模式完全不一样。录音回放之后我才发现,自己回答问题喜欢绕弯子,先说一堆背景才给结论。面试官的耐心是有限的,正确做法是先给结论,再展开细节。这一点不改,真实面试要吃大亏。
3.2 追问二:ThreadLocal为什么内存泄漏
第二个没答好的是ThreadLocal的内存泄漏。我能说出"ThreadLocalMap的key是弱引用,value是强引用,所以会泄漏",但当面试官追问"为什么弱引用反而会导致泄漏"时,我解释不清楚。回来重新梳理,这次彻底弄明白了。
ThreadLocal的每个线程里都有一个ThreadLocalMap,它的Entry继承WeakReference,key是ThreadLocal对象,value是开发者set进去的值。key被设计成弱引用的本意是:当外部不再持有ThreadLocal对象时,key可以被GC回收。但问题在于value是强引用,key被回收后,Entry变成"key为null、value还活着"的状态,如果线程不销毁,这个value就永远没机会被回收。
什么场景最容易触发?线程池。线程池里的线程会复用,生命周期很长,ThreadLocalMap一直挂在线程上,value就一直在内存里。解决方式也简单:用完必须remove。规范写法是用try-finally包裹:
ThreadLocal<Something> local = new ThreadLocal<>(); try { local.set(value); // 业务逻辑 } finally { local.remove(); // 必须清理,防止线程池场景下的内存泄漏 }这个问题的颗粒度,今天算彻底补齐了。从"知道会泄漏"到"能讲清楚为什么泄漏、什么场景触发、怎么预防",这才是面试要的深度。
3.3 追问三:现场给你一个死锁,你会怎么查
最后一个追问是:线上服务突然卡住,所有请求都不响应,怀疑是死锁,你会怎么排查?我之前只准备了死锁的定义(两个线程互相持有对方需要的锁),真到排查环节就懵了。面试官点了一句"你会用什么命令",我才想起jstack。
排查死锁的标准流程其实很固定,背下来就是肌肉记忆。先用jps找到Java进程ID,再用jstack输出线程快照,jstack本身会在末尾自动检测死锁,输出一行"Found one Java-level deadlock"。那段输出里会明确指出哪两个线程互相等待哪把锁。实际操作命令如下:
jps -l jstack <pid> > thread_dump.txt # 在dump文件里搜索 "deadlock" 关键字除了死锁,这个命令还能顺手查另外两类线上问题:线程长时间BLOCKED说明锁竞争激烈;线程大量WAITING且堆栈停在等待队列上,可能是线程池配置不合理。很多线上问题第一现场都在线程快照里,Day51的求职者最好能做到手写jps和jstack的用法。我也顺手补了jmap -heap看堆内存、jstat -gcutil看GC频率,这些在面试聊JVM调优时都会用到。
模拟面试结束后,我把三个短板记进了错题本,每一条都写了一句话版标准答案,方便早晚过一遍。错题本这个习惯从Day20坚持到现在,已经积累了四十多条,考前翻一遍,比临时抱佛脚刷新知识有效得多。
4. 晚间算法:两道手热题的复盘与刷题节奏管理
4.1 二叉树的最近公共祖先,递归解法拆解
算法部分今晚做了两道。第一道是"二叉树的最近公共祖先",是一道出现频率很高的树类题目。题目描述很简单:给定一个二叉树和两个节点p、q,找到它们的最近公共祖先。
递归解法核心是定义清晰:在当前节点root这棵子树里找p和q的最近公共祖先,找到就返回该祖先节点,找不到但子树中包含p或q就返回对应节点,都不包含返回null。代码可以写得非常短:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) { return root; } TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if (left != null && right != null) { return root; } return left != null ? left : right; }这道题的关键在于理解递归返回值的三层含义:返回null表示这个子树里既没有p也没有q;返回p或q表示子树中找到了其中一个;返回其他节点,说明这个节点就是p和q的最近公共祖先。面试时如果只把代码写出来而不解释返回值语义,很容易被追问到语法细节上卡壳。
进阶版本(二叉搜索树的最近公共祖先)可以用更简单的解法,利用BST的节点值大小关系,从根节点向下走,找到第一个值介于p和q之间的节点就是答案。这个变体很容易被拿来作为追问,最好一并准备。我当时把两个版本的代码放在同一页笔记里对比记忆,效果不错。
4.2 无重复字符的最长子串,滑窗思路的边界处理
第二道是"无重复字符的最长子串",滑窗的经典题。这类题目的通用套路是:右指针不断向右扩展,把字符加入窗口;用一个哈希表记录字符最后出现的位置;遇到重复字符时,左指针跳到重复字符上一次出现位置的下一个位置;每次右指针移动后更新答案。
public int lengthOfLongestSubstring(String s) { Map<Character, Integer> map = new HashMap<>(); int left = 0; int maxLen = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (map.containsKey(c)) { left = Math.max(left, map.get(c) + 1); // left只向右跳,不可能回退 } map.put(c, right); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; }这里有一个很容易写错的细节:左指针更新时为什么要用Math.max而不是直接left = map.get(c) + 1?因为在滑窗过程中,left可能已经超过某个重复字符上一次出现的位置,此时如果把left回退到更小的位置,窗口内会出现新的重复字符。这类边界问题,光看题解体会不到,自己多写错几次就记住了。我第一次提交就是在这个地方踩坑,用了一个反而把窗口扩大的bug用例才反应过来。
4.3 第51天的刷题节奏:两新两旧,复盘优先
进入第51天,我的刷题节奏已经固定成"两新两旧":每天两道新题,两道之前做错的旧题,周日停下来完整复盘本周的错题。这个安排看起来慢,实际上比每天狂刷五道新题高效得多。错题重做时,如果第二遍还要看题解,说明第一遍就没彻底掌握,需要再标记、隔一周再重做;如果独立写出且时间达标,才算真正过关。
刷题还有一个容易被忽视的细节:一定要写注释,标注每道题的核心思路和易错点。一个月后翻看,相当于有一本自己的算法错题集。面试前一天不需要重新刷题,把错题集过一遍就够了。
5. 简历项目怎么讲,才能在一分钟内建立区分度
5.1 三个维度:业务理解、技术难点、量化数据
晚上花了两个小时重新打磨项目的讲述逻辑。到了Day51这个阶段,项目已经没有时间做大改了,能优化的主要是"怎么讲"。同样是做过一个商城项目,有人讲得像流水账,有人能讲出亮点,差距在这三个维度。
第一是业务理解。你能不能介绍清楚这个项目解决的是谁的问题、业务流程的完整链路是什么、核心表结构怎么设计的、为什么订单和库存要分表。业务理解不行,技术细节讲得再花哨,面试官也只当你在背项目。我准备项目时给自己列了一个问题清单:这个系统有几个核心角色、每个角色有哪些操作、这些操作涉及哪些表、表的关联关系是什么。能把这些问题不假思索答出来,业务这一关才算过了。
第二是技术难点。项目里一定要有一两个能讲深的技术点。比如秒杀场景你怎么防超卖、高并发下你怎么保证缓存与数据库的一致性、分布式场景下订单号怎么做全局唯一。难点不在于多,而在于你能不能把背后的原理讲透,包括为什么用这个方案、还有什么替代方案、各自的缺陷是什么。我给自己定的标准是:准备两个技术难点,每个难点都要能讲到三个层次——方案是什么、为什么选它、它有什么局限。
第三是量化数据。没有数据的项目描述是没有说服力的。不要说"系统性能提升了",要说"经过优化后接口压测QPS从200提升到1500,P99响应时间从480ms降到120ms"。数据从哪里来?没有线上环境就自己压测,本地JMeter压测也能跑出数据。面试官听到具体数字,印象分会高不少。
5.2 一段"自我演示"的话术拆解
我给自己设计了一个模板,按"背景-职责-难点-结果"四步来讲一个模块,大概控制在三分钟以内。拿一个典型的订单超时关闭功能举例:
背景是项目里有一个用户下单后30分钟未支付就自动关闭订单的需求,最初实现方案是定时任务每分钟扫描一次订单表,但订单量大之后扫描效率很低,而且存在bug风险。我的职责是把这个方案改造成延迟消息机制。难点有两个,第一是怎么保证消息不丢失,第二是怎么在分布式环境下避免同一个订单被重复关闭。我的做法是消息队列延迟消息加消费端幂等校验,压测结果是订单关闭的及时率从平均两分钟提升到秒级,重复关闭的问题完全消除。
这段话术的要点是:先给结论再讲过程,用数字兜底,主动交代方案的不足。面试官听完会觉得你既有思考深度,又有落地能力。建议每个人都根据自己的项目写一个这样的版本,反复练习到不用看稿。
5.3 面试官最常追问的三个问题,提前准备答案
项目讲完之后,面试官一定会有追问。我把高频追问整理成了三类。
第一类:为什么选这个技术方案?比如"为什么用消息队列而不是定时任务",这个问题考察的就是你做技术选型时的思考过程。标准回答思路是从两个维度对比:实时性要求和资源消耗。定时任务的实时性差,扫描间隔决定了延迟上限;消息队列可以做到事件驱动,几乎实时触发,但引入了一个中间件,增加了运维复杂度。说出这两者的取舍关系,比只夸消息队列好用强得多。
第二类:这个方案的瓶颈在哪里?正确做法是自己指出短板,并说明改进思路。比如"延迟消息依赖消息队列,如果消息队列挂了,订单就可能一直不关闭,所以我会加一个兜底定时任务,扫描超时未支付且没有关闭的订单"。主动交代短板并给出兜底方案,反而会让面试官放心。
第三类:如果数据量涨十倍,你的方案还扛得住吗?这类问题考察的是可扩展性意识。回答时不要慌,可以从水平扩展、读写分离、缓存分层等方向展开。重点是让面试官看到你有这个意识,而不是真的要求你现在就把新方案设计出来。
6. 求职中后期避坑清单:来自踩坑者的直接忠告
6.1 学习资源怎么选:做减法,而不是囤积
第51天回头看,我在学习资源上踩过最大的坑是贪多。收藏夹里躺着几十个课程和学习资料,真正刷完的没几个。求职准备进入中后期,资源选择的原则应该是做减法:主线技术各选一份系统的资料,配合官方文档和源码,最多再加一份高质量的面经汇总,其余的收藏等于没收藏。
面经的使用方法也有讲究。不要只把题目和答案抄下来背,正确做法是拿到一道题,先不看答案,自己写一版回答,再对着参考修正。只有自己组织过的答案才记得牢。我前面提到的错题本,就是面经消化后的产物。
6.2 时间分配:别用战术勤奋掩盖战略懒惰
很多人在这个阶段仍然把大量时间花在学新知识上,这是很危险的。Day51的正确时间分配应该是:六成时间复习和深挖已有知识,三成时间模拟面试和整理错题,一成时间保持算法手感。如果你还在花大量时间看新的技术栈,说明前期知识地图铺得不够,这时候更要停下来补短板,而不是继续往前赶。
我每天晚上的例行动作是:翻一遍当天的错题本,计划明天的三个任务。任务控制在三个以内,完成一个勾掉一个。这个习惯帮我避免了很多次"感觉学了一天,回想起来什么都没学"的情况。Day51这天也不例外,明天的任务已经列好了:重写一遍线程池的实现流程、整理JVM调优命令、重做两道链表错题。
6.3 心态崩盘自救指南:Day51的倦怠期怎么过
走到第51天,倦怠感是真实存在的。你可能会觉得知识越学越多、忘得越来越快,也可能因为某次模拟面试的糟糕表现而怀疑自己。我自己的几个自救方法,分享给同样在坚持的人。
第一,把目标切小。不要总想着"我要进某家公司",太重了,容易压垮人。把目标改成"今天要把线程池流程讲清楚""明天要把这个项目的缓存一致性方案整理完",小目标完成带来的正反馈,比大目标管用得多。
第二,主动找人交流。一个人闷头复习很容易陷入自我怀疑,找到一个水平相当的朋友互相模拟面试、互相检查错题,效率会成倍提升。我今天的模拟面试就是这种交流的产物。
第三,接受遗忘是正常的。记忆的本质是遗忘后的再提取,复习一遍忘一些、再复习再忘,反复几轮之后才真正变成长期记忆。不要因为今天忘了昨天的内容就焦虑,这恰恰说明你在正常地学习。
我在模拟面试里暴露短板的时候,第一反应确实是有点沮丧,但冷静下来想想,这其实是好事。面试前暴露的问题越多,真实面试时踩的坑就越少。Day51的意义不在于你完成了多少学习量,而在于你终于开始像一个求职者那样思考:不是"我学了什么",而是"面试官会怎么问、我怎么答得更好"。
最后分享一个这几十天坚持下来的小技巧:每周日晚用半小时写一篇本周复盘,只写三个内容——本周搞定的三个知识点、本周暴露的两个短板、下周最想补的一个方向。这个节奏让我在第51天回头看时,能清楚看到自己从Day1到现在走过了多远。走到这里不容易,剩下的事就是把这些天的积累,稳稳地倒出来。