news 2026/10/12 14:39:35

synchronized轻量级锁会自旋吗?锁升级与适应性自旋深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
synchronized轻量级锁会自旋吗?锁升级与适应性自旋深度解析

1. 一个看似简单的面试题,为什么能问倒一片人

前两天在技术群里有人甩了一个问题过来:“Java 中的 synchronized 轻量级锁是否会进行自旋?”群里立刻分成了两派。一派背书式地回复“会自旋,自旋失败后膨胀为重量级锁”,另一派则搬出“轻量级锁其实没有自旋循环,竞争会直接膨胀”的说法。两边谁也说服不了谁,最后索性开始贴源码链接。

这其实是一个非常典型的“半懂不懂”问题。答“会”的人,大概率是看过几篇锁升级的文章,记住了触发顺序;答“不会”的人,可能啃过一部分 HotSpot 源码,但没把上下文串起来。真正能回答好这个问题,需要同时理解三件事:Mark Word 是什么、锁升级到底怎么发生、自旋代码究竟写在哪个阶段。缺一块,结论就容易跑偏。

所以这篇文章我想从头把这根线理顺。我不会只给一个“会”或“不会”的结论,而是把 HotSpot 里 synchronized 的加锁路径、轻量级锁的 CAS 流程、膨胀成 ObjectMonitor 之后的自旋逻辑,以及 JDK 版本差异都展开讲一遍。无论你是准备面试,还是想搞清楚锁调优该怎么下手,都应该能从中拿到能直接用的结论。

先剧透一下核心观点:**在主流 HotSpot 实现里,轻量级锁这个状态本身没有专门的自旋循环;真正发生的自旋,是在轻量级锁竞争触发膨胀之后,进入重量级锁 ObjectMonitor 的路径上。**但很多面试八股文讲的“轻量级锁自旋失败才升级”,是把这个过程简化包装之后的说法。严格说来,这种表述是不准确的,不过面试时怎么回答才能既不抬杠又显得有深度,我会在第 4 节给你一套完整话术。

2. 先拆锁升级链路:轻量级锁在 HotSpot 里究竟怎么加锁

2.1 Mark Word 就是锁状态的“身份证”

Java 对象的对象头里有一块区域叫 Mark Word,HotSpot 用它同时承担哈希码、GC 分代年龄、偏斜锁线程 ID、轻量级锁 Lock Record 指针、重量级锁 ObjectMonitor 指针等多项职责,通过末尾两位锁标志位来区分当前状态。

可以简单理解成一张纸条:纸条上现在写的是“无锁 + 哈希码”,还是一行“指向线程栈中 Lock Record 的地址”,又或者是一行“指向 ObjectMonitor 的地址”,决定了这把锁接下来的行为。

锁状态与 Mark Word 的大致对应关系如下:

锁状态Mark Word 内容示意获取方式适用场景
无锁哈希码 + age + 01不需要加锁步骤无竞争
偏向锁线程ID + epoch + 01只在 Mark Word 记录线程ID单线程反复访问同一对象
轻量级锁指向栈中 Lock Record + 00栈帧中 CAS 夺取锁多线程交替访问、竞争不激烈
重量级锁指向 ObjectMonitor + 10进入 monitor 的 enter 流程高竞争、多线程同时争抢

这张表的最后一行很关键。很多人以为“重量级锁”是某个锁对象直接变了,实际上它是指 Mark Word 指向了一个 JVM 内部的数据结构 ObjectMonitor,之后线程的阻塞、唤醒、自旋都围绕这个 monitor 展开。

2.2 轻量级锁的加锁三步:Lock Record 才是主角

轻量级锁的加锁过程,核心不是“拿一把锁出来”,而是把锁信息塞进当前线程栈帧里一个叫 Lock Record 的结构。HotSpot 的 BasicObjectLock / BasicLock 就是这个结构的底层实现。

大致过程是这样的:

  1. 当前线程在自己的栈帧里分配一个 Lock Record,里面留出一个 displaced header 的槽位。
  2. 把对象当前的 Mark Word(比如无锁状态下的哈希码和 age)保存到这个 displaced header 里,相当于给现场留个底。
  3. 执行 CAS 操作,尝试把对象 Mark Word 从“无锁状态”改为“指向当前线程 Lock Record 的地址”,同时把锁标志位改为 00。

如果 CAS 成功,说明轻量级锁已经拿到手,锁对象还是那个对象,只是 Mark Word 指向了线程内部结构。解锁时再做一次反向 CAS,把 displaced header 里的内容写回 Mark Word,锁就释放了,恢复成无锁状态。

如果 CAS 失败,则分两种情况。第一种是当前线程已经持有锁,比如同一个线程里 synchronized 重入,那么 Lock Record 里的 displaced header 会被标记为空,说明是可重入,不需要再抢一次。第二种是确实有其他线程正在持有锁,这时轻量级锁这条路已经走不通,JVM 会调用 inflate 对它进行膨胀,试图升级成重量级锁。

这个流程里值得注意的一点是:**在 HotSpot 的 slow_enter 路径中,轻量级锁的 CAS 失败后并没有一个“先原地重试 N 次”的循环。**它要么成功,要么因为重入而完成,要么直接进入锁膨胀流程。很多文章里画的“自旋重试”在这里并不存在。

如果你拿源码读,会看到一个简化后的流程长这样:

// 伪代码,用于理解执行顺序,不是 HotSpot 源码 if (mark.isNeutral()) { lockRecord.setDisplacedHeader(mark); if (obj.casSetMark(lockRecordAddress, mark)) { return; // 轻量级锁加锁成功 } } if (mark.hasLockRecord() && mark.locker() == currentThread) { lockRecord.setDisplacedHeader(null); // 重入 return; } // 来到这里说明有真实竞争 ObjectMonitor* monitor = inflate(obj, mark); monitor->enter(currentThread); // 自旋和阻塞都发生在 monitor 里

这段伪逻辑解释了为什么我说轻量级锁状态本身没有自旋。你看 inflate 之后紧接着就进入了 monitor 的 enter,后续的争夺、自旋、阻塞全部发生在 ObjectMonitor 这一层。

2.3 为什么设计轻量级锁,而不是遇事就上 monitor

理解完机制,你可能会问:既然竞争会膨胀到重量级锁,那轻量级锁的存在意义是什么?

答案是开销。轻量级锁全程只用用户态的 CAS 和栈帧操作,不涉及内核态,不需要创建额外的同步对象。只要锁竞争不激烈,这个路线远比直接丢给 ObjectMonitor 快。偏向锁更进一步,它连 CAS 都省了,直接比对线程 ID,专门给单线程重复访问的场景做优化。

轻量级锁适合“多个线程交替进入临界区”,比如两个线程各自在不同时间访问同一个共享对象,每次进来时锁早就释放了,线程只需要 CAS 一下就能成功。这时候如果直接上重量级锁,就会白白付出 monitor 分配、系统调用、上下文切换的代价,属于杀鸡用牛刀。

但轻量级锁也怕另一个极端:两个线程同时卡在临界区入口疯狂竞争。CAS 失败率一高,轻量级锁的优劣势就反转了,JVM 才会选择膨胀成重量级锁,让实际的等待逻辑接管。所以锁升级本质上是在不同竞争烈度下选择成本最低的方案。

另外要提醒一点:平时说的“锁升级只升不降”,严格讲并不绝对。偏向锁在触发批量撤销时可能会恢复到无锁状态,轻量级锁无竞争时解锁也能回到无锁。真正不可逆的是膨胀之后的重量级锁,一个对象一旦膨胀成 monitor,通常就不会再缩回轻量级锁了。面试时如果被问到“锁可以降级吗”,能分开讲清楚这两种情况,会有加分。

3. 自旋的真正落点:ObjectMonitor 的进入流程与适应性自旋

3.1 膨胀到底做了什么

锁膨胀 inflate 做的事,是把一个对象从“轻量级锁/无锁”状态,变成“Mark Word 指向 ObjectMonitor”的状态。ObjectMonitor 内部维护了 owner 字段、递归计数、EntryList 等待队列、WaitSet 等结构,参与竞争和等待的线程信息都记录在这里。

为什么 CAS 失败时不直接让线程阻塞,而是要经过膨胀?因为 JVM 需要一个更强大的数据结构来兜底。轻量级锁只有 Lock Record 和 Mark Word 之间的 CAS 关系,处理不了“一堆线程排队等锁、锁超时、线程 wait/notify、可重入计数”这些需求。这些能力都在 ObjectMonitor 里。

从对象头的角度看,膨胀前后最明显的差异就是 Mark Word 最后两位:轻量级锁是 00,重量级锁是 10。这也是第 5 节用 jol 做实验时判断锁状态的重要依据。

3.2 monitor enter 路径上的自旋循环

ObjectMonitor 的 enter 流程,才是 synchronized 自旋真正发生的地方。整个过程可以概括为三段:

  1. 先尝试快速获取:通过 CAS 尝试把 monitor 的 owner 从 null 设成当前线程。运气好,一次就拿到了,不需要进入后续流程。
  2. 适应性自旋:如果快速获取失败,线程不会马上 park 去睡觉,而是进入一段自旋循环。循环里反复检查 owner 是否变化,只要发现 owner 被释放,就再次尝试抢夺 owner。
  3. 阻塞等待:自旋达到一定条件还没成功,线程才会把当前节点加入 EntryList,然后通过 park 机制挂起,等待被持有锁的线程唤醒。

第二步就是大家常说的 synchronized 自旋。它的核心思想是:线程刚把锁让出来,唤醒和阻塞的代价可能比短暂的自旋更大,尤其是临界区很小、持锁时间极短的时候。与其立刻切换线程上下文,不如先在 CPU 上排队等一会儿,说不定锁马上就释放了。

这里必须再次强调位置关系:自旋循环发生在膨胀后的 ObjectMonitor::enter 里,不是在轻量级锁阶段的 Mark Word CAS 里。轻量级锁所谓的“自旋”,实际上是“竞争触发膨胀 + monitor 进入时自旋”这个组合效果。很多教程为了流程好记,直接说成“轻量级锁自旋失败后膨胀成重量级锁”,造成了概念混淆。

3.3 适应性自旋:不是死循环撞运气

JDK 6 之前,HotSpot 的自旋更像是一种固定次数的重试,可以用-XX:PreBlockSpin指定一个固定迭代数。但这种方式太僵硬,锁竞争激烈的系统调小了没效果,调大了又白烧 CPU。

JDK 6 之后引入的适应性自旋 Adaptive Spinning 改变了思路。JVM 会根据同一个锁之前自旋成功的历史记录动态调整自旋时长,逻辑上很像“信任评分”:自旋成功率高的锁,下次多转几圈;成功率低的锁,下次少转几圈,甚至直接跳过自旋进入等待。这套机制由 ObjectMonitor 内部维护的自旋统计参数驱动,不同对象能表现出完全不同的自旋节奏。

实际调优时,不太建议普通应用去碰-XX:PreBlockSpin。它是老版本留下的参数,在新版本里还保留着兼容入口,但生效与否、默认值多少,不同 JDK 小版本都可能不一样。真想确认你当前环境的情况,最可靠的办法是执行:

java -XX:+PrintFlagsFinal -version | grep -i 'PreBlockSpin\|UseAdaptiveSpinning'

在自己手头的 JDK 上跑一下,比背参数表靠谱得多。

那么回到标题本身,怎么用一句话精确定义答案?我会这样说:

如果问 HotSpot 中的轻量级锁状态本身是否会自旋,答案是不会。轻量级锁的 CAS 失败会直接走向膨胀,自旋发生在膨胀后的 ObjectMonitor 进入流程中,并且是适应性自旋。

这句话既回答了“会不会”,又把自旋的位置说清楚了,后面无论面试官怎么深挖,你都有东西可讲。

4. 面试答案怎么写:标准话术与四个高频误区

4.1 一段经得起追问的面试答案

面试时最怕的不是观点错,而是只背结论、经不起追问。关于这道题,我建议你在心里准备一段“分层答案”:

  1. 先给结论:在 HotSpot 里,synchronized 的锁升级路径是无锁、偏向锁、轻量级锁、重量级锁四个状态。
  2. 说明轻量级锁的加锁本质:通过栈帧中的 Lock Record 和对象 Mark Word 之间的 CAS 完成的,竞争触发膨胀。
  3. 指出自旋位置:膨胀成重量级锁后,ObjectMonitor 的 enter 过程会先尝试 CAS,CAS 失败后进行适应性自旋,自旋失败才 park。
  4. 补充版本和参数:JDK 6 以后默认使用适应性自旋,具体用-XX:PrintFlagsFinal可在当前 JDK 上确认。
  5. 如果有余力,再补一句 JDK 15 之后偏向锁默认被关闭(JEP 374),锁路径和旧版本有差异。

这段话的好处是,每一层都对应着一个可以继续深挖的问题。面试官问 Mark Word 结构你能答,问自旋实现你能答,问锁升级路径你能答,问参数调优你也能答。

4.2 误区一:轻量级锁自旋失败后才膨胀

这是最广泛流传的说法,也是很多人答错这题的根源。准确讲,轻量级锁的 CAS 失败会直接导致锁膨胀,膨胀之后 monitor 才开始自旋。所以不是“先在轻量级锁上自旋若干次,再考虑升级”,而是“CAS 失败后马上走膨胀流程,自旋是膨胀后的事情”。

为什么这个误区流传得这么广?一方面是因为早期很多锁升级的科普文章为了简化,把 monitor 进入流程里的自旋也算成“轻量级锁阶段”的优化;另一方面是因为 JVM 的设计目标本来就是隐藏内部路径,从外部看,你确实会观察到线程在进入临界区前短暂等待然后成功,至于是薄锁还是 monitor 自旋带来的成功,肉眼根本分辨不出来。

4.3 误区二:自旋次数是固定写死的

网上经常能看到“自旋次数默认 10 次”之类的说法。这种说法片面了。如果你在 JDK 8 上看-XX:PreBlockSpin的默认值,确实能看到类似 10 的数字,但那是老模型里固定自旋的入口。JDK 6 之后大部分场景走的是适应性自旋,次数会随监控数据动态改变。

更关键的是,真实的自旋循环不一定按“转动一次”计数,而是会结合 owner 释放事件、自旋失败比例、当前 CPU 核数等信息动态调整。你要是按固定系数去调PreBlockSpin,在新版本里很可能会发现没什么效果。

4.4 误区三:synchronized 的锁竞争一定产生阻塞

还有不少同学认为,只要出现锁竞争,线程状态就应该是 BLOCKED,配合 jstack 能立刻抓到。实际上,如果锁竞争发生在 monitor 的自旋阶段,线程还在 CPU 上空转,jstack 显示的很可能是 RUNNABLE,直到真正 park 挂起后才变成 BLOCKED 或 WAITING。

所以在排查线上问题时会有一个陷阱:看到大量线程 RUNNABLE,不一定是 CPU 在执行业务代码,也可能是正卡在锁自旋里。要区分这两者,光靠 jstack 一两张快照往往不够,还得结合 JFR、async-profiler 或者更长时间地采样分析原生栈,才能定位到 synchronized 的自旋热区。

4.5 误区四:只要用 synchronized 就是重量级锁

这个误区在我早期带团队时特别常见。很多人看到 synchronized 就把它和“重锁、慢、阻塞”划等号,忽略了 JVM 的锁升级机制。实际上,一个锁对象在没有竞争的时候可能连偏向锁都不需要,单线程反复进入可能一直停在偏向锁或轻量级锁状态,开销非常低。

如果把锁状态和锁是否发生系统调用混为一谈,就很难理解为什么现代 JVM 上 synchronized 的性能并不是“无脑差”。锁膨胀、自旋、park/unpark 这一整套机制,最终目的都是让锁在不同竞争烈度下自适应成本。

下面这张表把常见说法和源码视角的事实放在一起,方便对照记忆:

常见说法源码层事实
轻量级锁会自旋,失败后升级轻量级锁 CAS 失败直接膨胀;自旋在 monitor enter 里
自旋次数固定默认是适应性自旋,动态调整
竞争就阻塞线程自旋阶段线程仍在运行,park 后才阻塞
synchronized 都是重量级锁无竞争时可能是偏向锁或轻量级锁

5. 亲手验证一次:用 jol 观察对象头与锁膨胀

5.1 为什么推荐用 jol 而不是瞎猜

说了这么多原理,肯定有人想知道怎么亲眼看一看锁状态的切换。这里我推荐用 OpenJDK 官方出品的 jol(Java Object Layout)工具。它能直接打印对象头布局,包括 Mark Word 的内容和锁标志位,是观察锁状态最直观的手段之一。

在 Maven 项目里加一个依赖:

<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.17</version> </dependency>

然后写一个最简单的实验程序。

5.2 实验一:单线程持锁时的对象头变化

import org.openjdk.jol.info.ClassLayout; public class LockHeaderDemo { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); System.out.println("初始无锁:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); synchronized (lock) { System.out.println("单线程持锁:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } System.out.println("解锁后:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } }

在 JDK 8 默认开启偏向锁的情况下,第一次 synchronized 可能直接进入偏向锁,锁标志位显示 01,但 Mark Word 里不再是普通哈希码,而是记录了当前线程 ID。如果你希望跳过偏向锁这一步,只看轻量级锁,可以在启动参数里加上:

-XX:-UseBiasedLocking

这样单线程持锁时,你会看到 Mark Word 的锁标志位变成 00,整个 Mark Word 指向的是某个栈地址,这就是 Lock Record 的地址。解锁后再打印,锁标志位会回到 01,哈希码重新出现。

这里要提醒一句:不同 JDK 版本打印出的 Mark Word 数值格式有差异,JDK 8、JDK 11、JDK 17 之间的输出看起来可能完全不一样。不要死盯某一个数值,重点看锁标志位和对象头描述格式。

5.3 实验二:多线程高竞争下膨胀到 monitor

再看一个更接近标题问题的实验:多个线程同时抢同一把锁,制造高竞争,然后再看对象头。

import org.openjdk.jol.info.ClassLayout; public class MonitorInflateDemo { public static void main(String[] args) throws InterruptedException { Object lock = new Object(); for (int i = 0; i < 8; i++) { new Thread(() -> { long end = System.currentTimeMillis() + 500; while (System.currentTimeMillis() < end) { synchronized (lock) { // 模拟短暂临界区 } } }).start(); } Thread.sleep(1000); System.out.println("高竞争后对象头:"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } }

在高竞争跑完之后,锁对象的 Mark Word 大概率会变成锁标志位 10,同时 Mark Word 里出现一个指向 ObjectMonitor 的地址。这正好对应我们前面讲的流程:轻量级锁 CAS 竞争到一定烈度后,JVM 把它膨胀成了重量级锁,后续线程全部走 monitor 的 enter 路径。

如果你想在这个基础上继续观察自旋,可以配合 JFR 的线程阻塞事件,或者用 async-profiler 看看锁对象的 native 栈,会发现一段正处于自旋阶段的线程状态:它还没有 park,但栈顶已经被 ObjectMonitor 相关的 native 方法占据。这一步能得到比 jstack 更准确的证据。

5.4 用 PrintFlagsFinal 确认当前 JDK 的自旋参数

每个跑实验的人,JDK 版本都可能不同,所以我在团队里一直强调“先确认环境,再下结论”。最省事的方法就是:

java -XX:+PrintFlagsFinal -version

然后过滤出锁相关的参数,比如UseBiasedLocking、PreBlockSpin、UseHeavyMonitors等等。同一个参数在 JDK 8 和 JDK 17 里的默认值甚至语义可能都不一样。很多网上讨论吵得不可开交,本质上就是 JDK 版本不一致导致的。

6. 锁竞争调优的边界:知道自旋在哪之后,再谈怎么优化

6.1 自旋不是“越少越好”,也不是“越多越好”

理解了自旋发生在哪一环,最容易产生的冲动就是去调参。我自己早期也干过这种事,把一个压测系统的-XX:PreBlockSpin从默认值反复调大调小,最后发现效果不是线性的。自旋时间太短,线程频繁走 park/unpark,上下文切换成本上升;自旋时间太长,CPU 被无意义占住,真正干活的线程可能调度不过来。适应性自旋的价值就在于,它把“调参”这件事交给 JVM 运行时统计,一般场景下没必要手动干预。

如果你非要在高并发场景下调,别只看自旋次数,先去看锁竞争率、临界区大小、线程上下文切换频率这些指标。多数情况下,把临界区缩短、把锁粒度拆细,效果远好过调某几个 JVM 参数。

6.2 逃逸分析和锁消除也会影响自旋

还有一个经常被忽略的因素:JIT 的逃逸分析。如果锁对象根本没有逃逸出方法,JVM 可能会直接消除 synchronized,锁都不加,自然也就谈不上自旋和膨胀。这就是为什么本地写几行 synchronized,性能表现和线上高并发场景完全不同。

知道这一点,对面试也有帮助。别人问“锁升级这么复杂,那 JVM 会不会自己优化掉?”你可以顺势把逃逸分析、锁消除、锁粗化这几个概念接上。要注意的是,JIT 优化受到代码形态和运行时采样影响,结果不是稳定可预测的,所以“用 jol 观察到锁被消除了”这种实验并不能覆盖所有并发场景。

6.3 我个人的一点实操体会

这个题目我见过太多次了,也和很多不同背景的工程师对过线。给我的直观感受是:能在面试里把这道题答得漂亮的人,往往不是记住了“会”或“不会”,而是能把 Mark Word、Lock Record、ObjectMonitor、适应性自旋这几个节点串成一条完整的链路。链路上的任意一环都可以展开继续问,而这个展开能力才是壁垒。

最后分享一个我在地时比较常用的验证思路:如果你在一个高吞吐场景里怀疑 synchronized 成了瓶颈,不要急着换并发工具,先抓一把线程快照,确认绝大多数线程是饿在锁等待里,还是随时能进临界区。如果是后者,锁本身未必有锅,问题可能出在临界区太长。搞明白自旋在哪,其实是在帮你建立判断锁性能问题的坐标系,坐标系对了,后面怎么调都不太会跑偏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 14:38:19

open-code-review:开源自动化代码审查方案,让Code Review聚焦真正问题

做开发这些年&#xff0c;赶上过不少关于 code review 的争论。有人说它是质量最后的防线&#xff0c;有人说它就是形式主义&#xff0c;大家互相发个 LGTM 就算完事。我自己更倾向于第三种判断&#xff1a;code review 的关键根本不在“要不要做”&#xff0c;而在于“怎么让机…

作者头像 李华
网站建设 2026/10/12 14:35:05

x32dbg与x64dbg调试器选型指南:从位数判断到断点脚本实战

简介&#xff1a;x32dbg 与 x64dbg 是 Windows 平台广受欢迎的调试器工具&#xff0c;本资源即为两者的整合包&#xff0c;面向逆向工程初学者、安全研究人员及需要分析 32 位与 64 位程序的开发者&#xff0c;帮助解决调试环境搭建繁琐、组件缺失的问题。压缩包共收录 151 个文…

作者头像 李华
网站建设 2026/10/12 14:33:56

CppDepend实战:C/C++代码依赖分析、质量度量与构建门禁

简介&#xff1a;CppDepend 是一款面向 C 开发团队的静态代码分析工具&#xff0c;尤其适合剖析大型项目或遗留代码体系&#xff0c;帮助开发者快速厘清模块依赖、识别过度耦合并评估可维护性。资源包以 zip 格式封装&#xff0c;大小约 49.28MB&#xff0c;内含程序运行所需的…

作者头像 李华
网站建设 2026/10/12 14:32:53

snap7与MFC工业上位机开发:S7通信集成、线程模型与稳定性实战

1. 为什么还要折腾 snap7 与 MFC 这套组合工业上位机这个圈子有个很拧巴的现实&#xff1a;新项目都在喊 OPC UA、MQTT、RESTful&#xff0c;但真到了车间现场&#xff0c;你面对的大概率还是一台跑了十来年的西门子 S7-1200 或者 S7-300&#xff0c;网口就在那&#xff0c;协议…

作者头像 李华
网站建设 2026/10/12 14:28:24

编译原理课设实战:DFA词法分析与LALR(1)语法分析器实现

简介&#xff1a;这份资源面向高校计算机专业修读编译原理课程的学生与课程设计开发者&#xff0c;提供一套完整的C实现方案&#xff0c;涵盖基于DFA的词法分析器与基于LALR(1)的语法分析器&#xff0c;帮助读者理解从记号序列到抽象语法树构建的完整流程&#xff0c;并掌握act…

作者头像 李华