[{"data":1,"prerenderedAt":5310},["ShallowReactive",2],{"java:kafka-high-concurrency-performance-availability\u002F":3,"java-wiki-navigation":1081},{"id":4,"title":5,"body":6,"commentId":1067,"description":1068,"difficulty":1069,"draft":1070,"extension":1071,"meta":1072,"navigation":1073,"order":1074,"path":1075,"section":1076,"seo":1077,"stem":1078,"updated":1079,"__hash__":1080},"java\u002Fjava\u002Fkafka-high-concurrency-performance-availability.md","Kafka 是如何实现高并发、高性能、高可用的",{"type":7,"value":8,"toc":1024},"minimark",[9,13,17,37,40,48,51,54,70,84,88,93,96,106,109,116,120,123,126,137,140,144,147,150,156,159,163,166,169,173,177,180,186,189,193,196,202,222,225,229,232,238,241,255,262,266,269,275,278,295,302,308,311,314,321,324,351,361,365,368,371,385,402,406,409,415,421,427,430,439,445,448,455,461,465,468,471,477,479,485,488,494,497,500,503,518,521,525,529,532,538,541,544,548,551,557,560,570,573,579,582,585,589,594,605,608,622,625,628,634,637,641,648,654,657,660,714,717,723,726,730,733,736,742,745,749,811,814,820,824,827,847,850,853,859,862,866,870,873,877,883,887,890,894,897,904,912,916,919,930,934,948,951,965,968,1020],[10,11,5],"h1",{"id":12},"kafka-是如何实现高并发高性能高可用的",[14,15,16],"h2",{"id":16},"面试回答",[18,19,20],"blockquote",{},[21,22,23,24,28,29,32,33,36],"p",{},"Kafka 的高并发主要依靠分区实现横向扩展：一个 Topic 被拆成多个 Partition，分区 Leader 分散在不同 Broker 上，生产者可以并行写入，消费者组也可以按分区并行消费。高性能主要来自追加写和顺序 I\u002FO、操作系统 Page Cache、生产与消费两端的批处理、批量压缩，以及通过 ",[25,26,27],"code",{},"sendfile"," 实现的零拷贝传输。高可用则依靠多副本、Leader\u002FFollower、ISR、故障选主和 KRaft Controller Quorum；生产端再配合 ",[25,30,31],{},"acks=all","、",[25,34,35],{},"min.insync.replicas"," 和幂等生产，消费端通过 Consumer Group、Offset 与重新分配实现故障恢复。Kafka 的核心设计是在分区粒度上并行，在单分区内部维持有序日志，再用副本机制保障故障后的连续服务。",[21,38,39],{},"一句话总结：",[18,41,42],{},[21,43,44],{},[45,46,47],"strong",{},"分区提供并行度，顺序日志、批处理和零拷贝提供吞吐量，副本、ISR 与故障选主提供可用性。",[14,49,50],{"id":50},"详细讲解",[21,52,53],{},"Kafka 的三个能力不是彼此孤立的：",[55,56,57,61,64,67],"ul",{},[58,59,60],"li",{},"分区既是并发执行和横向扩展的单位，也是副本复制与故障恢复的单位。",[58,62,63],{},"批处理既减少网络请求，也把大量小写入转化为更高效的顺序写。",[58,65,66],{},"Page Cache 既提升读写性能，也能配合副本机制避免为了可靠性而对每条消息执行同步刷盘。",[58,68,69],{},"副本增强了可靠性，但同时会增加网络、磁盘与确认延迟，因此需要在吞吐、延迟和可靠性之间取舍。",[21,71,72],{},[73,74,80],"a",{"href":75,"target":76,"rel":77},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-high-concurrency-performance-availability.svg","_blank",[78,79],"noopener","noreferrer",[81,82],"img",{"src":75,"alt":83},"Kafka 高并发、高性能和高可用实现机制总览",[14,85,87],{"id":86},"一高并发通过分区横向扩展","一、高并发：通过分区横向扩展",[89,90,92],"h3",{"id":91},"_1-partition-是-kafka-的并行单元","1. Partition 是 Kafka 的并行单元",[21,94,95],{},"一个 Topic 可以拆成多个 Partition：",[97,98,104],"pre",{"className":99,"code":101,"language":102,"meta":103},[100],"language-text","Topic: order-events\n├── Partition 0 → Leader 在 Broker A\n├── Partition 1 → Leader 在 Broker B\n└── Partition 2 → Leader 在 Broker C\n","text","",[25,105,101],{"__ignoreMap":103},[21,107,108],{},"不同分区可以由不同 Broker 同时处理，因此整体吞吐不再受限于单台机器。增加 Broker 并合理增加分区后，存储容量、网络带宽和读写能力都可以横向扩展。",[21,110,111,112,115],{},"Kafka 只保证",[45,113,114],{},"单分区内有序","，不保证一个 Topic 的所有分区之间全局有序。这是它获得并行能力的重要前提。",[89,117,119],{"id":118},"_2-生产者直接找到分区-leader","2. 生产者直接找到分区 Leader",[21,121,122],{},"生产者会缓存集群元数据，根据消息 Key、显式分区或默认分区策略选择 Partition，然后直接向该分区的 Leader 发送数据，中间不需要额外的统一路由节点。",[21,124,125],{},"常见分区方式：",[55,127,128,131,134],{},[58,129,130],{},"指定 Key：相同 Key 通常进入同一分区，适合保证同一订单、用户或账户内的消息顺序。",[58,132,133],{},"不指定 Key：默认分区策略会在适当时机选择分区，并尽量形成较大的批次。",[58,135,136],{},"自定义分区器：按租户、地区或业务规则路由。",[21,138,139],{},"如果 Key 分布不均，大量消息集中到少数分区，就会出现热点分区。此时即使 Broker 很多，整体吞吐仍会被热点 Leader 限制。",[89,141,143],{"id":142},"_3-consumer-group-按分区并行消费","3. Consumer Group 按分区并行消费",[21,145,146],{},"同一个 Consumer Group 中，一个 Partition 在同一时刻只分配给一个 Consumer；一个 Consumer 可以负责多个 Partition。",[21,148,149],{},"例如：",[97,151,154],{"className":152,"code":153,"language":102,"meta":103},[100],"4 个 Partition + 2 个 Consumer\n→ 每个 Consumer 负责约 2 个 Partition\n\n4 个 Partition + 4 个 Consumer\n→ 每个 Consumer 负责约 1 个 Partition\n\n4 个 Partition + 6 个 Consumer\n→ 最多只有 4 个 Consumer 能获得 Partition\n",[25,155,153],{"__ignoreMap":103},[21,157,158],{},"因此，一个消费组的有效并行度上限通常受 Partition 数量约束。简单增加 Consumer，而不增加 Partition，不一定能提高吞吐量。",[89,160,162],{"id":161},"_4-broker-内部通过线程分工处理请求","4. Broker 内部通过线程分工处理请求",[21,164,165],{},"Broker 不会用一个线程串行完成所有网络和磁盘工作。网络线程负责接收请求、解析协议和返回响应，请求处理线程负责执行 Produce、Fetch 等具体逻辑，后台线程负责日志清理、副本同步等任务。",[21,167,168],{},"这种职责分离让网络连接管理、业务请求处理和后台维护能够并行执行，避免某类工作完全阻塞其他工作。",[14,170,172],{"id":171},"二高性能把随机小操作变成顺序批量操作","二、高性能：把随机小操作变成顺序批量操作",[89,174,176],{"id":175},"_1-顺序追加写","1. 顺序追加写",[21,178,179],{},"Kafka 的 Partition 本质上是一条只追加的日志。新消息写到日志末尾，不需要像通用数据库那样频繁进行随机位置更新。",[97,181,184],{"className":182,"code":183,"language":102,"meta":103},[100],"旧数据 ────────────────→ 日志末尾追加新批次\noffset 0  1  2  3  4  5  6 ...\n",[25,185,183],{"__ignoreMap":103},[21,187,188],{},"顺序 I\u002FO 能充分利用磁盘和操作系统的预读、合并写能力。日志达到一定大小或时间后会切分为新的 Segment，旧 Segment 可以按保留策略清理或压缩，不影响当前日志继续追加。",[89,190,192],{"id":191},"_2-分段日志与稀疏索引","2. 分段日志与稀疏索引",[21,194,195],{},"每个 Partition 由多个日志 Segment 组成，常见文件包括：",[97,197,200],{"className":198,"code":199,"language":102,"meta":103},[100],"00000000000000000000.log\n00000000000000000000.index\n00000000000000000000.timeindex\n",[25,201,199],{"__ignoreMap":103},[55,203,204,210,216],{},[58,205,206,209],{},[25,207,208],{},".log"," 保存消息批次。",[58,211,212,215],{},[25,213,214],{},".index"," 保存相对 Offset 到日志物理位置的稀疏映射。",[58,217,218,221],{},[25,219,220],{},".timeindex"," 保存时间戳到相对 Offset 的稀疏映射。",[21,223,224],{},"查找消息时，Kafka 先定位 Segment，再通过稀疏索引找到接近目标的位置，最后在日志中继续顺序查找。索引不需要记录每一条消息，因此能控制索引体积和内存占用。",[89,226,228],{"id":227},"_3-充分利用-page-cache","3. 充分利用 Page Cache",[21,230,231],{},"Kafka 不把全部消息封装成大量 Java 堆对象长期缓存，而是依赖操作系统 Page Cache：",[97,233,236],{"className":234,"code":235,"language":102,"meta":103},[100],"Producer\n→ Broker 写日志文件\n→ 数据先进入 Page Cache\n→ 操作系统异步回写磁盘\n",[25,237,235],{"__ignoreMap":103},[21,239,240],{},"这样做有几个好处：",[55,242,243,246,249,252],{},[58,244,245],{},"利用操作系统成熟的预读和回写机制。",[58,247,248],{},"减少 JVM 堆内存占用和 GC 压力。",[58,250,251],{},"热数据可以直接从 Page Cache 读取。",[58,253,254],{},"Broker 重启后，操作系统缓存仍可能保留部分热数据，不必完全重新构建 JVM 内缓存。",[21,256,257,258,261],{},"Kafka 的可靠性主要依赖多副本，而不是对每条消息都执行一次同步 ",[25,259,260],{},"fsync","。如果强制每条消息同步刷盘，吞吐和延迟都会明显恶化。",[89,263,265],{"id":264},"_4-端到端批处理","4. 端到端批处理",[21,267,268],{},"批处理贯穿生产、Broker 存储和消费三段链路：",[97,270,273],{"className":271,"code":272,"language":102,"meta":103},[100],"多条消息\n→ Producer 聚合成 Record Batch\n→ 一次网络请求发送\n→ Broker 将 Record Batch 连续追加到分区日志\n→ Consumer 一次 Fetch 多条消息\n",[25,274,272],{"__ignoreMap":103},[21,276,277],{},"它可以摊薄：",[55,279,280,283,286,289,292],{},[58,281,282],{},"网络往返成本。",[58,284,285],{},"系统调用成本。",[58,287,288],{},"协议头和请求头开销。",[58,290,291],{},"磁盘写入次数。",[58,293,294],{},"消息压缩成本。",[21,296,297,298,301],{},"这里的“Broker 追加批次”不是把多条消息合并成一条业务消息，也不承诺底层一定只发生一次系统调用。它表示 Kafka 的生产者和存储格式都以 ",[45,299,300],{},"Record Batch"," 为基本单位：",[97,303,306],{"className":304,"code":305,"language":102,"meta":103},[100],"Record Batch\n├── 批次头：baseOffset、长度、时间戳、压缩类型、CRC 等\n├── Record 1\n├── Record 2\n└── Record 3\n",[25,307,305],{"__ignoreMap":103},[21,309,310],{},"Producer 会先按 Partition 在内存中积累消息，形成 Record Batch。Broker 收到后，对批次进行校验，为其中的消息确定 Offset，然后把整段连续数据追加到对应 Partition 的当前日志 Segment 末尾。相比每条消息都单独发请求、单独写日志，批次追加可以用更少的请求和更大的连续 I\u002FO 摊薄固定开销。",[21,312,313],{},"一个 ProduceRequest 还可以携带多个 Partition 的 Record Batch，但 Broker 最终仍然分别追加到各个 Partition 的日志中。因此：",[18,315,316],{},[21,317,318],{},[45,319,320],{},"批次是网络传输和日志写入的基本单位，Partition 才是日志组织、顺序保证和副本复制的基本单位。",[21,322,323],{},"生产者常见相关参数：",[97,325,329],{"className":326,"code":327,"language":328,"meta":103,"style":103},"language-properties shiki shiki-themes github-light github-dark","batch.size=16384\nlinger.ms=5\ncompression.type=lz4\n","properties",[25,330,331,339,345],{"__ignoreMap":103},[332,333,336],"span",{"class":334,"line":335},"line",1,[332,337,338],{},"batch.size=16384\n",[332,340,342],{"class":334,"line":341},2,[332,343,344],{},"linger.ms=5\n",[332,346,348],{"class":334,"line":347},3,[332,349,350],{},"compression.type=lz4\n",[21,352,353,356,357,360],{},[25,354,355],{},"batch.size"," 和 ",[25,358,359],{},"linger.ms"," 体现的是吞吐与延迟之间的取舍：适度等待可以积累更大的批次，但等待过长会增加低流量场景的发送延迟。参数值应根据消息大小、流量和延迟目标压测确定，不能机械照搬示例。",[89,362,364],{"id":363},"_5-批量压缩","5. 批量压缩",[21,366,367],{},"Kafka 对一个 Record Batch 进行整体压缩，而不是分别压缩每条消息。相似消息中的字段名和公共内容可以获得更好的压缩率。",[21,369,370],{},"压缩后的批次会以压缩形式写入日志并传输给消费者，从而降低：",[55,372,373,376,379,382],{},[58,374,375],{},"Broker 磁盘占用。",[58,377,378],{},"生产者到 Broker 的网络流量。",[58,380,381],{},"副本复制流量。",[58,383,384],{},"Broker 到消费者的网络流量。",[21,386,387,388,32,391,32,394,397,398,401],{},"压缩会消耗 CPU，因此需要根据资源瓶颈选择 ",[25,389,390],{},"lz4",[25,392,393],{},"zstd",[25,395,396],{},"snappy"," 或 ",[25,399,400],{},"gzip"," 等算法。网络或磁盘是瓶颈时，压缩往往能换取更高吞吐。",[89,403,405],{"id":404},"_6-零拷贝","6. 零拷贝",[21,407,408],{},"消费者拉取日志数据时，普通路径可能需要：",[97,410,413],{"className":411,"code":412,"language":102,"meta":103},[100],"磁盘 → Page Cache → 用户空间 → Socket Buffer → 网卡\n",[25,414,412],{"__ignoreMap":103},[21,416,417,418,420],{},"Kafka 在适用条件下利用 Linux ",[25,419,27],{},"，让数据从 Page Cache 直接进入网络发送路径，减少用户态和内核态之间的复制与上下文切换：",[97,422,425],{"className":423,"code":424,"language":102,"meta":103},[100],"Page Cache → Socket \u002F 网卡\n",[25,426,424],{"__ignoreMap":103},[21,428,429],{},"这就是常说的零拷贝。它主要优化 Broker 向消费者传输已有日志数据的过程。",[21,431,432],{},[73,433,436],{"href":434,"target":76,"rel":435},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-sendfile-vs-traditional-io.svg",[78,79],[81,437],{"src":434,"alt":438},"Kafka sendfile 零拷贝与传统文件传输路径对比",[21,440,441,442,444],{},"传统路径需要先把数据从内核 Page Cache 复制到 Broker 的用户空间，再从用户空间写回内核 Socket Buffer；",[25,443,27],{}," 让内核直接组织 Page Cache 到网络 Socket 的传输，Broker 不再把消息内容完整搬入 JVM 用户空间。",[21,446,447],{},"所以“零拷贝”更准确的含义是：",[18,449,450],{},[21,451,452],{},[45,453,454],{},"减少或绕过 Page Cache 与用户空间之间不必要的数据复制，而不是数据在磁盘、内存和网卡之间一次都不移动。",[21,456,457,458,460],{},"需要注意，Kafka 官方文档明确说明，启用 SSL 时数据要经过用户态 TLS 处理，当前不会使用这条 ",[25,459,27],{}," 路径。因此不能笼统地说所有 Kafka 网络传输都一定是零拷贝。",[89,462,464],{"id":463},"_7-consumer-pull-与长轮询","7. Consumer Pull 与长轮询",[21,466,467],{},"Kafka 由消费者主动 Pull 数据。消费者可以根据自己的处理能力决定拉取速度，处理不过来时，未消费消息会暂时保留在 Kafka 中，并表现为 Consumer Lag 增长，而不是由 Broker 持续 Push 直至压垮消费者。",[21,469,470],{},"Consumer Lag 表示消费者的消费进度落后于 Partition 最新位置的程度。监控消费组时，通常可以近似理解为：",[97,472,475],{"className":473,"code":474,"language":102,"meta":103},[100],"Consumer Lag\n= Partition Log End Offset\n- Consumer Group 已提交 Offset\n",[25,476,474],{"__ignoreMap":103},[21,478,149],{},[97,480,483],{"className":481,"code":482,"language":102,"meta":103},[100],"Partition 下一条待写位置：10000\n消费组下一条待消费位置：9400\n\nLag = 10000 - 9400 = 600\n",[25,484,482],{"__ignoreMap":103},[21,486,487],{},"这表示当前大约还有 600 个 Offset 的数据尚未被该消费组处理。Lag 不是消息丢失，而是消费积压：",[97,489,492],{"className":490,"code":491,"language":102,"meta":103},[100],"生产速度 > 消费速度\n→ Lag 持续增大\n\n消费速度 > 生产速度\n→ Consumer 逐步追赶\n→ Lag 逐步减小\n",[25,493,491],{"__ignoreMap":103},[21,495,496],{},"Lag 不能只看某一时刻的绝对值，还要观察增长趋势、积压持续时间和业务允许的最大延迟。同样数量的 Lag，在每秒几条和每秒几十万条的 Topic 中代表的时间延迟完全不同。",[21,498,499],{},"如果 Lag 长期增长，可能是消费者处理变慢、下游故障、分区热点、Consumer 数量不足或频繁重平衡。若积压时间超过 Topic 的数据保留期限，旧消息可能已经被清理，消费者就不能再依靠 Kafka 把这部分数据完整追回。",[21,501,502],{},"Fetch 请求支持批量拉取和长轮询：",[97,504,506],{"className":326,"code":505,"language":328,"meta":103,"style":103},"fetch.min.bytes=1\nfetch.max.wait.ms=500\n",[25,507,508,513],{"__ignoreMap":103},[332,509,510],{"class":334,"line":335},[332,511,512],{},"fetch.min.bytes=1\n",[332,514,515],{"class":334,"line":341},[332,516,517],{},"fetch.max.wait.ms=500\n",[21,519,520],{},"Broker 暂时没有足够数据时，可以等待一段时间再响应，避免消费者无数据时频繁空轮询；流量较高时，又可以一次返回较大的批次。",[14,522,524],{"id":523},"三高可用副本isr-与故障转移","三、高可用：副本、ISR 与故障转移",[89,526,528],{"id":527},"_1-每个-partition-可以有多个副本","1. 每个 Partition 可以有多个副本",[21,530,531],{},"假设副本因子为 3：",[97,533,536],{"className":534,"code":535,"language":102,"meta":103},[100],"Partition 0\n├── Leader：Broker A\n├── Follower：Broker B\n└── Follower：Broker C\n",[25,537,535],{"__ignoreMap":103},[21,539,540],{},"生产和普通消费请求由 Leader 处理，Follower 分别从 Leader 拉取并复制日志。多个 Follower 的同步是彼此独立的，不存在先同步 B、再由 B 同步 C 的固定顺序。",[21,542,543],{},"副本应尽量分散在不同 Broker；具备机架感知配置时，还可以跨机架放置，降低单机或单机架故障带来的影响。",[89,545,547],{"id":546},"_2-isr-维护可安全选主的副本集合","2. ISR 维护可安全选主的副本集合",[21,549,550],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。Follower 故障或长时间落后时会被移出 ISR，重新追上后可以再次加入。",[97,552,555],{"className":553,"code":554,"language":102,"meta":103},[100],"正常：ISR = [A, B, C]\nC 落后：ISR = [A, B]\n",[25,556,554],{"__ignoreMap":103},[21,558,559],{},"Leader 故障后，Controller 会从符合条件的副本中选择新的 Leader。以 ISR 为基础进行选主，可以避免把明显缺少最新日志的副本直接作为新的数据源。",[89,561,563,564,567,568],{"id":562},"_3-acks-与-mininsyncreplicas","3. ",[25,565,566],{},"acks"," 与 ",[25,569,35],{},[21,571,572],{},"常见的高可靠组合是：",[97,574,577],{"className":575,"code":576,"language":102,"meta":103},[100],"replication.factor = 3\nmin.insync.replicas = 2\nacks = all\n",[25,578,576],{"__ignoreMap":103},[21,580,581],{},"含义是生产者等待当前 ISR 的确认，并且 ISR 至少要有两个副本，否则 Broker 拒绝写入。",[21,583,584],{},"这个配置体现了 CAP 取舍：副本不足时拒绝写入，会降低部分可用性，但可以避免在只剩单副本时继续写入并承担更高的数据丢失风险。",[89,586,588],{"id":587},"_4-kraft-controller-quorum-保证控制面可用","4. KRaft Controller Quorum 保证控制面可用",[590,591,593],"h4",{"id":592},"kraft-controller-quorum-是什么","KRaft Controller Quorum 是什么",[21,595,596,597,600,601,604],{},"KRaft 是 Kafka 自己实现的元数据管理机制，用来替代早期依赖的 ZooKeeper。集群中会有若干个承担 ",[25,598,599],{},"controller"," 角色的节点，这些 Controller 组成一个基于 Raft 共识协议的 ",[45,602,603],{},"Controller Quorum","，即控制器仲裁组。",[21,606,607],{},"它们维护的是集群元数据，而不是普通 Topic 的业务消息：",[55,609,610,613,616,619],{},[58,611,612],{},"Broker 注册、存活状态和节点信息。",[58,614,615],{},"Topic 与 Partition 的定义。",[58,617,618],{},"每个 Partition 的副本分配、Leader 和 ISR。",[58,620,621],{},"配置、配额以及其他集群级元数据。",[21,623,624],{},"这些变化会写入 KRaft 的集群元数据日志，并复制给 Quorum 中的其他 Controller。一次元数据变更获得多数 Controller 确认后才算提交，因此单个 Controller 故障不会导致已提交元数据丢失。",[21,626,627],{},"常见部署是 3 个或 5 个 Controller：",[97,629,632],{"className":630,"code":631,"language":102,"meta":103},[100],"3 个 Controller → 多数派为 2，可容忍 1 个故障\n5 个 Controller → 多数派为 3，可容忍 2 个故障\n",[25,633,631],{"__ignoreMap":103},[21,635,636],{},"如果存活 Controller 失去多数派，集群不能安全提交新的元数据变更。此时已有数据读写是否还能短暂继续，取决于具体操作和现有元数据状态，但创建 Topic、Partition Leader 调度等控制面操作会受到影响。",[590,638,640],{"id":639},"active-controller-是什么","Active Controller 是什么",[21,642,643,644,647],{},"Controller Quorum 在同一时刻会选出一个 Leader，Kafka 的文档和运行语境中通常称它为 ",[45,645,646],{},"Active Controller","。其他 Controller 作为 Follower 复制元数据日志，随时准备接管。",[97,649,652],{"className":650,"code":651,"language":102,"meta":103},[100],"Controller A：Active Controller\n├── 接收和处理元数据变更\n├── 管理 Broker 注册与故障\n├── 触发 Partition Leader 选举\n└── 把变更写入元数据日志\n\nController B、C：Follower Controllers\n├── 复制元数据日志\n└── Active Controller 故障后参与重新选举\n",[25,653,651],{"__ignoreMap":103},[21,655,656],{},"Active Controller 不是所有业务消息的入口，也不负责替 Broker 保存普通 Topic 数据。生产者和消费者仍然直接与 Partition Leader 所在的 Broker 通信。",[21,658,659],{},"需要明确区分两个层面：",[661,662,663,682],"table",{},[664,665,666],"thead",{},[667,668,669,673,676,679],"tr",{},[670,671,672],"th",{},"层面",[670,674,675],{},"主要角色",[670,677,678],{},"保存什么",[670,680,681],{},"负责什么",[683,684,685,700],"tbody",{},[667,686,687,691,694,697],{},[688,689,690],"td",{},"数据面",[688,692,693],{},"Broker、Partition Leader\u002FFollower",[688,695,696],{},"普通 Topic 的业务消息",[688,698,699],{},"生产、消费、副本复制",[667,701,702,705,708,711],{},[688,703,704],{},"控制面",[688,706,707],{},"KRaft Controller Quorum",[688,709,710],{},"集群元数据日志",[688,712,713],{},"节点管理、元数据变更、Leader 调度",[21,715,716],{},"例如 Broker A 突然故障：",[97,718,721],{"className":719,"code":720,"language":102,"meta":103},[100],"Active Controller 感知 Broker A 失联\n→ 找出受影响的 Partition\n→ 从符合条件的副本中选择新 Leader\n→ 将选举结果写入元数据日志\n→ 多数 Controller 确认提交\n→ 向相关 Broker 发布新元数据\n→ 客户端刷新元数据并访问新 Leader\n",[25,722,720],{"__ignoreMap":103},[21,724,725],{},"如果 Active Controller 自身故障，剩余 Controller 会通过 Raft 重新选出新的 Active Controller。新节点已经复制了已提交的元数据日志，因此可以继续管理集群。",[89,727,729],{"id":728},"_5-consumer-group-自动接管故障消费者的分区","5. Consumer Group 自动接管故障消费者的分区",[21,731,732],{},"消费者通过心跳维持组成员身份。如果某个 Consumer 崩溃或超时，Consumer Group 会重新分配它原来负责的 Partition，让其他 Consumer 接管。",[21,734,735],{},"消费进度保存在已提交的 Offset 中，因此新 Consumer 可以从相应位置继续处理：",[97,737,740],{"className":738,"code":739,"language":102,"meta":103},[100],"Consumer A 故障\n→ 触发重新分配\n→ Partition 交给 Consumer B\n→ B 从已提交 Offset 继续消费\n",[25,741,739],{"__ignoreMap":103},[21,743,744],{},"重平衡期间可能产生短暂停顿，所以高可用不等于无感切换。合理设置会话、心跳、处理时长，并避免频繁扩缩容，有助于减少重平衡影响。",[14,746,748],{"id":747},"四三种能力如何协同","四、三种能力如何协同",[661,750,751,767],{},[664,752,753],{},[667,754,755,758,761,764],{},[670,756,757],{},"目标",[670,759,760],{},"核心机制",[670,762,763],{},"解决的问题",[670,765,766],{},"主要代价",[683,768,769,783,797],{},[667,770,771,774,777,780],{},[688,772,773],{},"高并发",[688,775,776],{},"多 Partition、多 Broker、Consumer Group",[688,778,779],{},"并行生产、存储和消费",[688,781,782],{},"分区与元数据管理成本",[667,784,785,788,791,794],{},[688,786,787],{},"高性能",[688,789,790],{},"顺序写、Page Cache、批处理、压缩、零拷贝",[688,792,793],{},"降低 I\u002FO、复制和网络开销",[688,795,796],{},"批处理延迟与 CPU 消耗",[667,798,799,802,805,808],{},[688,800,801],{},"高可用",[688,803,804],{},"多副本、ISR、Controller Quorum、故障转移",[688,806,807],{},"节点故障后继续提供服务",[688,809,810],{},"副本存储、网络与确认延迟",[21,812,813],{},"可以把整个链路理解为：",[97,815,818],{"className":816,"code":817,"language":102,"meta":103},[100],"消息按 Partition 分流\n→ Producer 批量发送\n→ Leader 顺序追加到日志\n→ Follower 并行复制\n→ 满足确认条件后返回\n→ Consumer Group 按 Partition 批量拉取\n",[25,819,817],{"__ignoreMap":103},[14,821,823],{"id":822},"五为什么不能只靠增加分区提升性能","五、为什么不能只靠增加分区提升性能",[21,825,826],{},"分区是扩展能力的基础，但分区不是越多越好。分区数量增加会同时带来：",[55,828,829,832,835,838,841,844],{},[58,830,831],{},"更多日志目录、Segment 和文件句柄。",[58,833,834],{},"更多副本复制任务。",[58,836,837],{},"更大的集群元数据。",[58,839,840],{},"更高的 Leader 选举和故障恢复成本。",[58,842,843],{},"Consumer Group 重平衡开销。",[58,845,846],{},"单 Key 顺序范围更难调整。",[21,848,849],{},"合理做法是根据目标吞吐量、单分区压测能力、消费者并行度、副本数和未来增长空间估算，而不是一次创建大量分区。",[21,851,852],{},"一个简化估算思路：",[97,854,857],{"className":855,"code":856,"language":102,"meta":103},[100],"分区数 ≥ max(\n  目标生产吞吐 \u002F 单分区生产吞吐,\n  目标消费吞吐 \u002F 单消费者单分区吞吐,\n  期望消费并行度\n)\n",[25,858,856],{"__ignoreMap":103},[21,860,861],{},"最终仍需要在接近生产环境的机器、网络、副本配置和消息大小下进行压测。",[14,863,865],{"id":864},"六常见误区","六、常见误区",[89,867,869],{"id":868},"误区一kafka-使用磁盘所以一定比内存消息队列慢","误区一：Kafka 使用磁盘，所以一定比内存消息队列慢",[21,871,872],{},"Kafka 采用顺序追加、Page Cache 和批处理，大量读写实际在操作系统缓存中完成。性能取决于访问模式，而不是简单由“使用磁盘”决定。",[89,874,876],{"id":875},"误区二零拷贝让数据完全不发生复制","误区二：零拷贝让数据完全不发生复制",[21,878,879,880,882],{},"零拷贝是减少不必要的用户态复制，并非物理意义上的一次复制都没有；而且启用 SSL 时不会走 Kafka 文档描述的 ",[25,881,27],{}," 路径。",[89,884,886],{"id":885},"误区三消费者越多消费速度一定越快","误区三：消费者越多，消费速度一定越快",[21,888,889],{},"同一消费组内，一个 Partition 同时只能交给一个 Consumer。Consumer 数超过 Partition 数后，多出的 Consumer 不会获得分区。",[89,891,893],{"id":892},"误区四副本越多高可用越高且没有代价","误区四：副本越多，高可用越高且没有代价",[21,895,896],{},"更多副本会增加存储、网络复制和写确认成本。副本数应根据故障域和可靠性目标确定，常见三副本不代表所有场景都必须相同。",[89,898,900,901,903],{"id":899},"误区五acksall-就代表所有配置副本都已写入","误区五：",[25,902,31],{}," 就代表所有配置副本都已写入",[21,905,906,908,909,911],{},[25,907,31],{}," 等待的是当前 ISR，而不是所有配置副本。要避免 ISR 只剩一个副本时仍然成功写入，需要配合 ",[25,910,35],{},"。",[14,913,915],{"id":914},"七排查性能和可用性问题时看什么","七、排查性能和可用性问题时看什么",[89,917,918],{"id":918},"生产端",[55,920,921,924,927],{},[58,922,923],{},"发送吞吐、请求延迟、错误率和重试次数。",[58,925,926],{},"批次大小、压缩率和缓冲区等待。",[58,928,929],{},"消息 Key 是否造成分区流量倾斜。",[89,931,933],{"id":932},"broker","Broker",[55,935,936,939,942,945],{},[58,937,938],{},"各 Broker、Partition 和 Leader 的流量是否均衡。",[58,940,941],{},"磁盘利用率、磁盘延迟、网络带宽和请求队列。",[58,943,944],{},"ISR 收缩、未充分复制分区和离线分区。",[58,946,947],{},"Page Cache 命中效果和系统是否发生 Swap。",[89,949,950],{"id":950},"消费端",[55,952,953,956,959,962],{},[58,954,955],{},"Consumer Lag 及其增长速度。",[58,957,958],{},"单条消息处理耗时和批次处理耗时。",[58,960,961],{},"Consumer 数量与 Partition 数量是否匹配。",[58,963,964],{},"重平衡次数、心跳超时和 Offset 提交失败。",[14,966,967],{"id":967},"参考资料",[55,969,970,978,985,992,999,1006,1013],{},[58,971,972],{},[73,973,977],{"href":974,"rel":975},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fdesign\u002Fdesign\u002F",[976],"nofollow","Apache Kafka Design",[58,979,980],{},[73,981,984],{"href":982,"rel":983},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fkraft\u002F",[976],"Apache Kafka KRaft",[58,986,987],{},[73,988,991],{"href":989,"rel":990},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fimplementation\u002Fmessage-format\u002F",[976],"Apache Kafka Message Format",[58,993,994],{},[73,995,998],{"href":996,"rel":997},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fproducer-configs\u002F",[976],"Apache Kafka Producer Configs",[58,1000,1001],{},[73,1002,1005],{"href":1003,"rel":1004},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fconsumer-configs\u002F",[976],"Apache Kafka Consumer Configs",[58,1007,1008],{},[73,1009,1012],{"href":1010,"rel":1011},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fbroker-configs\u002F",[976],"Apache Kafka Broker Configs",[58,1014,1015],{},[73,1016,1019],{"href":1017,"rel":1018},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fclients\u002Fconsumer\u002FKafkaConsumer.html",[976],"Apache KafkaConsumer JavaDoc",[1021,1022,1023],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":103,"searchDepth":341,"depth":341,"links":1025},[1026,1027,1028,1034,1043,1051,1052,1053,1061,1066],{"id":16,"depth":341,"text":16},{"id":50,"depth":341,"text":50},{"id":86,"depth":341,"text":87,"children":1029},[1030,1031,1032,1033],{"id":91,"depth":347,"text":92},{"id":118,"depth":347,"text":119},{"id":142,"depth":347,"text":143},{"id":161,"depth":347,"text":162},{"id":171,"depth":341,"text":172,"children":1035},[1036,1037,1038,1039,1040,1041,1042],{"id":175,"depth":347,"text":176},{"id":191,"depth":347,"text":192},{"id":227,"depth":347,"text":228},{"id":264,"depth":347,"text":265},{"id":363,"depth":347,"text":364},{"id":404,"depth":347,"text":405},{"id":463,"depth":347,"text":464},{"id":523,"depth":341,"text":524,"children":1044},[1045,1046,1047,1049,1050],{"id":527,"depth":347,"text":528},{"id":546,"depth":347,"text":547},{"id":562,"depth":347,"text":1048},"3. acks 与 min.insync.replicas",{"id":587,"depth":347,"text":588},{"id":728,"depth":347,"text":729},{"id":747,"depth":341,"text":748},{"id":822,"depth":341,"text":823},{"id":864,"depth":341,"text":865,"children":1054},[1055,1056,1057,1058,1059],{"id":868,"depth":347,"text":869},{"id":875,"depth":347,"text":876},{"id":885,"depth":347,"text":886},{"id":892,"depth":347,"text":893},{"id":899,"depth":347,"text":1060},"误区五：acks=all 就代表所有配置副本都已写入",{"id":914,"depth":341,"text":915,"children":1062},[1063,1064,1065],{"id":918,"depth":347,"text":918},{"id":932,"depth":347,"text":933},{"id":950,"depth":347,"text":950},{"id":967,"depth":341,"text":967},"wiki:java:kafka-high-concurrency-performance-availability","从分区并行、顺序写、Page Cache、批处理、零拷贝、副本与 ISR 等机制，系统理解 Kafka 高吞吐与高可用架构。","advanced",false,"md",{},true,73,"\u002Fjava\u002Fkafka-high-concurrency-performance-availability","面试专题",{"title":5,"description":1068},"java\u002Fkafka-high-concurrency-performance-availability","2026-07-24","0TlKAnCJWs7ZZl90WzviZ4k1DZq74xMTxxLN4sHf-c8",[1082,2512,3419,4586],{"id":1083,"title":1084,"body":1085,"commentId":2502,"description":2503,"difficulty":1069,"draft":1070,"extension":1071,"meta":2504,"navigation":1073,"order":2505,"path":2506,"section":2507,"seo":2508,"stem":2509,"updated":2510,"__hash__":2511},"java\u002Fjava\u002Fjdk8-synchronized-lock-states.md","JDK 8 synchronized 设计与锁状态转换",{"type":7,"value":1086,"toc":2446},[1087,1091,1097,1108,1111,1117,1120,1129,1134,1137,1142,1148,1152,1155,1161,1168,1171,1177,1180,1183,1231,1235,1238,1244,1250,1253,1262,1267,1271,1278,1293,1407,1413,1419,1422,1425,1431,1434,1438,1444,1447,1453,1460,1463,1496,1500,1503,1506,1509,1513,1516,1519,1526,1529,1534,1538,1541,1547,1550,1553,1556,1562,1565,1568,1571,1577,1580,1586,1592,1596,1603,1606,1612,1615,1627,1630,1633,1636,1642,1645,1648,1651,1655,1658,1661,1665,1668,1672,1675,1679,1682,1685,1690,1694,1697,1699,1702,1705,1711,1714,1717,1720,1724,1738,1741,1747,1750,1756,1759,1765,1768,1772,1775,1779,1782,1785,1788,1794,1800,1804,1807,1813,1816,1822,1825,1828,1832,1835,1849,1852,1855,1861,1864,1870,1874,1881,1887,1890,1896,1899,1902,1916,1919,1923,1928,1934,1940,1949,1954,1958,1961,1964,1975,1978,1981,1984,1988,2164,2168,2172,2178,2182,2185,2191,2194,2198,2204,2208,2211,2245,2248,2299,2302,2328,2331,2337,2340,2346,2350,2354,2357,2361,2364,2368,2371,2375,2378,2382,2385,2389,2392,2396,2433,2436,2444],[14,1088,1090],{"id":1089},"一先给出整体结论","一、先给出整体结论",[21,1092,1093,1096],{},[25,1094,1095],{},"synchronized"," 是 Java 语言提供的内置同步机制。它在语义上负责三件事：",[55,1098,1099,1102,1105],{},[58,1100,1101],{},"同一时刻只允许满足条件的线程进入临界区；",[58,1103,1104],{},"支持同一线程重复获取同一把锁，也就是可重入；",[58,1106,1107],{},"解锁 happens-before 后续对同一 Monitor 的加锁，保证相关共享数据的可见性和有序性。",[21,1109,1110],{},"在 JDK 8 HotSpot 中，为了避免所有同步操作都直接阻塞线程，JVM 会根据竞争情况使用不同实现：",[97,1112,1115],{"className":1113,"code":1114,"language":102,"meta":103},[100],"偏向锁：同一个线程反复进入，尽量避免原子操作\n轻量级锁：线程交替执行或短暂竞争，使用栈上 Lock Record 和 CAS\n重量级锁：竞争较强或必须使用 Monitor 能力时，通过 ObjectMonitor 管理等待线程\n",[25,1116,1114],{"__ignoreMap":103},[21,1118,1119],{},"通常所说的“无锁、偏向锁、轻量级锁、重量级锁”，是对对象头及其关联同步结构的概括，不是 Java 语言规范定义的四种锁类型。",[21,1121,1122],{},[73,1123,1126],{"href":1124,"target":76,"rel":1125},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-state-transitions.svg",[78,79],[81,1127],{"src":1124,"alt":1128},"JDK 8 synchronized 锁状态转换总览",[18,1130,1131],{},[21,1132,1133],{},"图中信息较多，可点击图片查看原图。",[21,1135,1136],{},"需要先纠正一个常见说法：",[18,1138,1139],{},[21,1140,1141],{},"“锁只能升级，不能降级”适合帮助理解竞争路径，但并不严谨。",[21,1143,1144,1145],{},"轻量级锁正常释放后会通过 CAS 恢复对象原来的 Mark Word；已经膨胀的 ObjectMonitor 也可能在后续安全点被 JVM 回收。更准确的说法是：",[45,1146,1147],{},"持锁竞争过程中通常不会为了当前请求立即从重量级锁退回轻量级锁，但对象状态并非终身只能单向变化。",[14,1149,1151],{"id":1150},"二synchronized-在字节码中的入口","二、synchronized 在字节码中的入口",[21,1153,1154],{},"同步代码块通常编译为：",[97,1156,1159],{"className":1157,"code":1158,"language":102,"meta":103},[100],"monitorenter\n   临界区代码\nmonitorexit\n",[25,1160,1158],{"__ignoreMap":103},[21,1162,1163,1164,1167],{},"编译器会为正常返回和异常退出路径生成对应的 ",[25,1165,1166],{},"monitorexit","，保证退出同步块时释放锁。",[21,1169,1170],{},"同步方法不会在方法体中出现这两个指令，而是在方法访问标志中使用：",[97,1172,1175],{"className":1173,"code":1174,"language":102,"meta":103},[100],"ACC_SYNCHRONIZED\n",[25,1176,1174],{"__ignoreMap":103},[21,1178,1179],{},"JVM 在调用和返回同步方法时完成 Monitor 的进入与退出。",[21,1181,1182],{},"无论入口是哪一种，最终都围绕同一个锁对象工作：",[661,1184,1185,1195],{},[664,1186,1187],{},[667,1188,1189,1192],{},[670,1190,1191],{},"写法",[670,1193,1194],{},"实际锁对象",[683,1196,1197,1208,1220],{},[667,1198,1199,1202],{},[688,1200,1201],{},"实例同步方法",[688,1203,1204,1205],{},"当前实例 ",[25,1206,1207],{},"this",[667,1209,1210,1213],{},[688,1211,1212],{},"静态同步方法",[688,1214,1215,1216,1219],{},"当前类对应的 ",[25,1217,1218],{},"Class"," 对象",[667,1221,1222,1225],{},[688,1223,1224],{},"同步代码块",[688,1226,1227,1230],{},[25,1228,1229],{},"synchronized (...)"," 中指定的对象",[14,1232,1234],{"id":1233},"三对象头与-mark-word","三、对象头与 Mark Word",[21,1236,1237],{},"普通 Java 对象的对象头主要包含：",[97,1239,1242],{"className":1240,"code":1241,"language":102,"meta":103},[100],"Mark Word\nKlass Pointer\n",[25,1243,1241],{"__ignoreMap":103},[21,1245,1246,1247,1249],{},"数组对象还会额外保存数组长度。",[25,1248,1095],{}," 的关键状态主要编码在 Mark Word 中。",[21,1251,1252],{},"以 JDK 8、64 位 HotSpot 为例，Mark Word 会复用同一段空间：",[21,1254,1255],{},[73,1256,1259],{"href":1257,"target":76,"rel":1258},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-mark-word.svg",[78,79],[81,1260],{"src":1257,"alt":1261},"JDK 8 HotSpot Mark Word 与同步结构",[18,1263,1264],{},[21,1265,1266],{},"可点击图片查看完整尺寸。",[89,1268,1270],{"id":1269},"统一按最低-3-位阅读","统一按最低 3 位阅读",[21,1272,1273,1274,1277],{},"为了避免一会儿看 2 位、一会儿看 3 位，本文后续统一展示 Mark Word 的最低 ",[45,1275,1276],{},"3 位","。但要先区分“展示宽度”和“字段含义”：",[55,1279,1280,1287,1290],{},[58,1281,1282,1283,1286],{},"真正的锁标志始终是最低 ",[45,1284,1285],{},"2 位","；",[58,1288,1289],{},"只有普通未锁定和偏向布局会把倒数第 3 位解释成“偏向位”；",[58,1291,1292],{},"轻量级锁、重量级锁保存的是对齐后的指针，本文把其最低 3 位也完整写出，但倒数第 3 位不再表示偏向。",[661,1294,1295,1311],{},[664,1296,1297],{},[667,1298,1299,1302,1305,1308],{},[670,1300,1301],{},"最低 3 位",[670,1303,1304],{},"最低 2 位锁标志",[670,1306,1307],{},"状态",[670,1309,1310],{},"倒数第 3 位如何解释",[683,1312,1313,1334,1353,1371,1389],{},[667,1314,1315,1320,1325,1328],{},[688,1316,1317],{},[25,1318,1319],{},"001",[688,1321,1322],{},[25,1323,1324],{},"01",[688,1326,1327],{},"普通未锁定",[688,1329,1330,1331],{},"偏向位为 ",[25,1332,1333],{},"0",[667,1335,1336,1341,1345,1348],{},[688,1337,1338],{},[25,1339,1340],{},"101",[688,1342,1343],{},[25,1344,1324],{},[688,1346,1347],{},"可偏向或已偏向",[688,1349,1330,1350],{},[25,1351,1352],{},"1",[667,1354,1355,1360,1365,1368],{},[688,1356,1357],{},[25,1358,1359],{},"000",[688,1361,1362],{},[25,1363,1364],{},"00",[688,1366,1367],{},"轻量级锁",[688,1369,1370],{},"对齐后的 Lock Record 指针低位，不是偏向位",[667,1372,1373,1378,1383,1386],{},[688,1374,1375],{},[25,1376,1377],{},"010",[688,1379,1380],{},[25,1381,1382],{},"10",[688,1384,1385],{},"重量级锁",[688,1387,1388],{},"对齐后的 ObjectMonitor 指针低位，不是偏向位",[667,1390,1391,1396,1401,1404],{},[688,1392,1393],{},[25,1394,1395],{},"011",[688,1397,1398],{},[25,1399,1400],{},"11",[688,1402,1403],{},"GC 标记",[688,1405,1406],{},"标记状态的一部分，不是偏向位",[21,1408,1409,1410,1412],{},"因此 ",[25,1411,1340],{}," 应当拆开读成：",[97,1414,1417],{"className":1415,"code":1416,"language":102,"meta":103},[100],"偏向位 1 ｜ 锁标志 01\n",[25,1418,1416],{"__ignoreMap":103},[21,1420,1421],{},"它不是一个三位的“锁标志”。本文使用三位只是为了让所有状态在图表中对齐、便于比较。",[21,1423,1424],{},"其中偏向状态可以继续区分：",[97,1426,1429],{"className":1427,"code":1428,"language":102,"meta":103},[100],"匿名偏向：线程指针为空，允许第一个线程认领\n已偏向：线程指针指向获得偏向的 JavaThread\n",[25,1430,1428],{"__ignoreMap":103},[21,1432,1433],{},"Mark Word 是一块被复用的空间。例如普通未锁定对象可以直接保存 identity hash；偏向对象需要保存线程指针，因此二者不能同时使用同一布局。",[89,1435,1437],{"id":1436},"epoch-到底是什么","epoch 到底是什么",[21,1439,1440,1443],{},[25,1441,1442],{},"epoch"," 可以理解为某个类的“偏向版本号”，它不是时间戳，也不是对象年龄。",[21,1445,1446],{},"偏向锁不只依赖对象自身。支持偏向的类会在类的 prototype header 中保存一个当前 epoch；每个偏向对象的 Mark Word 中也会记录对象认领时使用的 epoch。线程进入偏向对象时，会比较两者：",[97,1448,1451],{"className":1449,"code":1450,"language":102,"meta":103},[100],"对象 epoch == 类当前 epoch\n    → 这份偏向记录仍属于当前版本，继续检查偏向线程\n\n对象 epoch != 类当前 epoch\n    → 这份偏向记录已经过期，可以进入重偏向或撤销流程\n",[25,1452,1450],{"__ignoreMap":103},[21,1454,1455,1456,1459],{},"它主要服务于",[45,1457,1458],{},"批量重偏向","。当一批对象从线程 A 整体转交给线程 B 使用时，JVM 不必立即逐个改写所有对象头，而是先提升类的 epoch。旧对象之后被访问时，会因为 epoch 不匹配而按新版本重新处理。",[21,1461,1462],{},"所以需要把两个容易混淆的字段分开：",[661,1464,1465,1475],{},[664,1466,1467],{},[667,1468,1469,1472],{},[670,1470,1471],{},"字段",[670,1473,1474],{},"含义",[683,1476,1477,1487],{},[667,1478,1479,1484],{},[688,1480,1481],{},[25,1482,1483],{},"age",[688,1485,1486],{},"对象的 GC 年龄，用于分代回收",[667,1488,1489,1493],{},[688,1490,1491],{},[25,1492,1442],{},[688,1494,1495],{},"类的偏向版本，用于判断对象上的偏向记录是否过期",[14,1497,1499],{"id":1498},"四jdk-8-为什么需要三种锁实现","四、JDK 8 为什么需要三种锁实现",[21,1501,1502],{},"不同竞争场景适合不同成本的同步方式。",[89,1504,1505],{"id":1505},"只有一个线程反复进入",[21,1507,1508],{},"如果一个对象长期只被同一个线程加锁，每次都执行 CAS 仍然是额外成本。偏向锁把对象与线程关联起来，后续同一线程进入时主要检查对象头是否仍偏向自己。",[89,1510,1512],{"id":1511},"多线程交替执行但没有同时竞争","多线程交替执行，但没有同时竞争",[21,1514,1515],{},"例如线程 A 使用完后线程 B 才进入。此时没有必要挂起线程，可以通过栈上 Lock Record 和 CAS 完成加锁、解锁。",[89,1517,1518],{"id":1518},"多线程同时争抢或需要等待队列",[21,1520,1521,1522,1525],{},"当 CAS 快速路径无法解决竞争，或者调用 ",[25,1523,1524],{},"wait()"," 需要条件等待集合时，JVM 使用 ObjectMonitor 管理 Owner、竞争线程和等待线程。",[21,1527,1528],{},"所以这三种实现的目标不是简单比较“谁更高级”，而是：",[18,1530,1531],{},[21,1532,1533],{},"根据实际竞争强度，在原子操作、CPU 自旋、线程阻塞和唤醒成本之间做权衡。",[14,1535,1537],{"id":1536},"五对象初始状态普通未锁定还是匿名偏向","五、对象初始状态：普通未锁定还是匿名偏向",[21,1539,1540],{},"JDK 8 默认启用偏向锁，但默认存在启动延迟：",[97,1542,1545],{"className":1543,"code":1544,"language":102,"meta":103},[100],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=4000\n",[25,1546,1544],{"__ignoreMap":103},[21,1548,1549],{},"因此需要区分对象创建时机：",[89,1551,1552],{"id":1552},"偏向锁尚未启用或被关闭",[21,1554,1555],{},"新对象通常使用普通未锁定布局：",[97,1557,1560],{"className":1558,"code":1559,"language":102,"meta":103},[100],"[identity hash | age | 0 | 01]\n",[25,1561,1559],{"__ignoreMap":103},[21,1563,1564],{},"线程第一次进入同步块时，直接尝试建立轻量级锁。",[89,1566,1567],{"id":1567},"偏向锁已经启用",[21,1569,1570],{},"支持偏向的类创建新对象时，对象通常处于匿名偏向状态：",[97,1572,1575],{"className":1573,"code":1574,"language":102,"meta":103},[100],"[0 | epoch | age | 1 | 01]\n",[25,1576,1574],{"__ignoreMap":103},[21,1578,1579],{},"因此严格来说，不应把这条路径描述为：",[97,1581,1584],{"className":1582,"code":1583,"language":102,"meta":103},[100],"新对象先是无锁，然后第一次进入才“升级”为可偏向状态\n",[25,1585,1583],{"__ignoreMap":103},[21,1587,1588,1589],{},"更准确的说法是：",[45,1590,1591],{},"对象创建时便根据类的 prototype header 获得普通未锁定或匿名偏向的 Mark Word。",[14,1593,1595],{"id":1594},"六偏向锁的获取与重入","六、偏向锁的获取与重入",[21,1597,1598,1599,1602],{},"线程进入一个匿名偏向对象时，会尝试通过 CAS 把自己的 ",[25,1600,1601],{},"JavaThread*","、类当前的 epoch 等信息写入 Mark Word。",[21,1604,1605],{},"成功后，对象变成：",[97,1607,1610],{"className":1608,"code":1609,"language":102,"meta":103},[100],"[当前 JavaThread* | epoch | age | 1 | 01]\n",[25,1611,1609],{"__ignoreMap":103},[21,1613,1614],{},"同一个线程以后再次进入时，主要检查：",[1616,1617,1618,1621,1624],"ol",{},[58,1619,1620],{},"Mark Word 是否仍是偏向模式；",[58,1622,1623],{},"偏向线程是否是当前线程；",[58,1625,1626],{},"对象 epoch 是否仍与类的 prototype header 匹配。",[21,1628,1629],{},"全部满足时可直接进入，不需要再次通过 CAS 抢占锁。",[89,1631,1632],{"id":1632},"偏向锁退出为什么不清除线程信息",[21,1634,1635],{},"偏向锁针对的就是“同一个线程还会再次进入”这一场景。同步块结束后，Mark Word 通常仍保留对原线程的偏向：",[97,1637,1640],{"className":1638,"code":1639,"language":102,"meta":103},[100],"进入前：偏向线程 A\n执行中：偏向线程 A\n退出后：仍偏向线程 A\n",[25,1641,1639],{"__ignoreMap":103},[21,1643,1644],{},"它不是表示线程 A 永远占用临界区，而是表示下一次线程 A 再进入时可以走低成本路径。",[89,1646,1647],{"id":1647},"偏向锁如何实现可重入",[21,1649,1650],{},"对象头记录了偏向线程，但不通过一个公共计数器记录每次进入。JVM 可以结合当前线程栈上的锁记录识别同步嵌套。对同一偏向线程而言，重复进入不需要竞争性地修改对象头。",[14,1652,1654],{"id":1653},"七其他线程访问偏向对象时发生什么","七、其他线程访问偏向对象时发生什么",[21,1656,1657],{},"线程 B 遇到偏向线程 A 的对象时，不能简单地直接覆盖线程指针。JVM 需要先判断原偏向是否仍然有效。",[21,1659,1660],{},"可能出现以下路径。",[89,1662,1664],{"id":1663},"_1-epoch-已过期","1. epoch 已过期",[21,1666,1667],{},"类发生批量重偏向后，旧对象的 epoch 可能落后于类的 epoch。线程 B 可以尝试通过 CAS 将对象重新偏向自己。",[89,1669,1671],{"id":1670},"_2-原线程已经不再持有这把锁","2. 原线程已经不再持有这把锁",[21,1673,1674],{},"JVM 可以撤销原偏向，使对象恢复为普通未锁定状态，或者在允许的情况下重新偏向新线程。",[89,1676,1678],{"id":1677},"_3-原线程仍在同步块中","3. 原线程仍在同步块中",[21,1680,1681],{},"JVM 需要检查原线程栈，撤销偏向并重建合法的锁状态。如果此时存在实际竞争，后续会进入轻量级竞争或直接膨胀为 ObjectMonitor。",[21,1683,1684],{},"因此：",[18,1686,1687],{},[21,1688,1689],{},"另一个线程访问偏向对象，不等于必然直接升级为重量级锁。是否重偏向、撤销为普通状态、转换为轻量级锁或膨胀，需要结合 epoch、类级启发式统计和原线程是否仍持锁判断。",[14,1691,1693],{"id":1692},"八批量重偏向与批量撤销","八、批量重偏向与批量撤销",[21,1695,1696],{},"如果某个类的大量对象不断被不同线程使用，逐个在安全点撤销偏向的成本会很高。HotSpot 会对同一类的偏向撤销次数做启发式统计。",[89,1698,1458],{"id":1458},[21,1700,1701],{},"当撤销达到一定阈值时，JVM 可以提升类 prototype header 中的 epoch。旧对象不需要立刻逐个修改；之后线程发现对象 epoch 过期时，再尝试将它偏向当前线程。",[21,1703,1704],{},"适合这种模式：",[97,1706,1709],{"className":1707,"code":1708,"language":102,"meta":103},[100],"一批对象先由线程 A 使用\n之后整体交给线程 B 使用\n但同一时刻竞争并不强\n",[25,1710,1708],{"__ignoreMap":103},[89,1712,1713],{"id":1713},"批量撤销",[21,1715,1716],{},"如果该类持续表现出多线程竞争，偏向锁已经不再适合，JVM 可以撤销该类的偏向能力。之后新对象不再默认可偏向，已有对象也会按正常锁路径处理。",[21,1718,1719],{},"这也是为什么偏向锁不能只按“对象自身的四级升级”理解：它还存在以类为粒度的 prototype header、epoch 和批量启发式机制。",[14,1721,1723],{"id":1722},"九普通未锁定到轻量级锁","九、普通未锁定到轻量级锁",[21,1725,1726,1727,1730,1731,1734,1735,1737],{},"当对象处于普通未锁定状态，线程进入同步块时会在当前栈帧创建锁记录，HotSpot 源码中对应 ",[25,1728,1729],{},"BasicLock","，解释器栈中通常由 ",[25,1732,1733],{},"BasicObjectLock"," 将对象引用和 ",[25,1736,1729],{}," 组织在一起。",[21,1739,1740],{},"概念流程如下：",[97,1742,1745],{"className":1743,"code":1744,"language":102,"meta":103},[100],"1. 在线程栈创建 Lock Record\n2. 将对象原 Mark Word 保存到 displaced header\n3. CAS 修改对象 Mark Word\n4. 让 Mark Word 指向当前 Lock Record\n",[25,1746,1744],{"__ignoreMap":103},[21,1748,1749],{},"CAS 成功后：",[97,1751,1754],{"className":1752,"code":1753,"language":102,"meta":103},[100],"对象 Mark Word  →  线程栈 Lock Record\nLock Record     →  保存原 Mark Word\n",[25,1755,1753],{"__ignoreMap":103},[21,1757,1758],{},"对象头低两位为：",[97,1760,1763],{"className":1761,"code":1762,"language":102,"meta":103},[100],"00\n",[25,1764,1762],{"__ignoreMap":103},[21,1766,1767],{},"这就是通常所说的轻量级锁或栈锁。",[89,1769,1771],{"id":1770},"为什么要保存-displaced-header","为什么要保存 displaced header",[21,1773,1774],{},"轻量级锁占用了对象原来的 Mark Word。解锁时需要把原 Mark Word 恢复回对象头，因此先把它保存到 Lock Record。",[14,1776,1778],{"id":1777},"十轻量级锁的可重入","十、轻量级锁的可重入",[21,1780,1781],{},"同一个线程再次获取已经由自己栈锁定的对象时，JVM 会识别对象头中的 Lock Record 指针属于当前线程栈。",[21,1783,1784],{},"此时新的 Lock Record 会使用特殊值表示递归进入，例如 displaced header 置空，而不是再次覆盖对象头。",[21,1786,1787],{},"退出时：",[97,1789,1792],{"className":1790,"code":1791,"language":102,"meta":103},[100],"递归层 Lock Record：只退出当前递归层\n最外层 Lock Record：负责真正恢复对象头\n",[25,1793,1791],{"__ignoreMap":103},[21,1795,1796,1797,1799],{},"这解释了为什么 ",[25,1798,1095],{}," 天然支持可重入。",[14,1801,1803],{"id":1802},"十一轻量级锁如何释放","十一、轻量级锁如何释放",[21,1805,1806],{},"最外层同步块退出时，线程使用 CAS 尝试把 Lock Record 中保存的 displaced header 恢复到对象 Mark Word：",[97,1808,1811],{"className":1809,"code":1810,"language":102,"meta":103},[100],"期望值：对象头仍指向当前 Lock Record\n新值：进入同步块前保存的原 Mark Word\n",[25,1812,1810],{"__ignoreMap":103},[21,1814,1815],{},"CAS 成功：",[97,1817,1820],{"className":1818,"code":1819,"language":102,"meta":103},[100],"轻量级锁正常释放\n对象恢复普通未锁定状态\n",[25,1821,1819],{"__ignoreMap":103},[21,1823,1824],{},"CAS 失败通常意味着对象已在竞争过程中膨胀，不能再直接把原对象头覆盖回去，需要走 ObjectMonitor 的退出逻辑。",[21,1826,1827],{},"这就是“锁只能升级不能降级”表述不够准确的直接例子：没有发生膨胀的轻量级锁，退出后会恢复原 Mark Word。",[14,1829,1831],{"id":1830},"十二轻量级锁何时膨胀","十二、轻量级锁何时膨胀",[21,1833,1834],{},"线程尝试用 CAS 建立轻量级锁失败，说明对象头已经发生变化。可能原因包括：",[55,1836,1837,1840,1843,1846],{},[58,1838,1839],{},"另一个线程持有轻量级锁；",[58,1841,1842],{},"对象已经膨胀为 ObjectMonitor；",[58,1844,1845],{},"竞争期间其他线程正在处理锁状态；",[58,1847,1848],{},"必须执行依赖 ObjectMonitor 的操作。",[21,1850,1851],{},"JVM 可以先进行短暂自旋，希望持锁线程很快退出。自旋避免了线程立即挂起和恢复，但会消耗 CPU，因此只适合临界区较短的情况。",[21,1853,1854],{},"当快速路径不能解决问题时，JVM 会执行 Monitor inflation：",[97,1856,1859],{"className":1857,"code":1858,"language":102,"meta":103},[100],"创建或取得 ObjectMonitor\n复制并保存原 Mark Word\n将对象头改为指向 ObjectMonitor\n由 ObjectMonitor 负责后续竞争\n",[25,1860,1858],{"__ignoreMap":103},[21,1862,1863],{},"对象头低两位变成：",[97,1865,1868],{"className":1866,"code":1867,"language":102,"meta":103},[100],"10\n",[25,1869,1867],{"__ignoreMap":103},[14,1871,1873],{"id":1872},"十三重量级锁与-objectmonitor","十三、重量级锁与 ObjectMonitor",[21,1875,1876,1877,1880],{},"重量级状态下，对象 Mark Word 指向一个 ",[25,1878,1879],{},"ObjectMonitor","。理解它时重点关注以下字段或逻辑角色：",[97,1882,1885],{"className":1883,"code":1884,"language":102,"meta":103},[100],"_owner       当前持有 Monitor 的线程或相关锁记录\n_recursions  重入次数\n_cxq         新到达竞争线程形成的竞争队列\n_EntryList   等待重新竞争 Owner 的线程\n_WaitSet     调用 wait 后等待通知的线程\n_header      膨胀前保存的对象 Mark Word\n",[25,1886,1884],{"__ignoreMap":103},[21,1888,1889],{},"概念结构：",[97,1891,1894],{"className":1892,"code":1893,"language":102,"meta":103},[100],"对象 Mark Word\n      │\n      ▼\nObjectMonitor\n  ├─ Owner\n  ├─ Recursions\n  ├─ cxq \u002F EntryList\n  ├─ WaitSet\n  └─ displaced header\n",[25,1895,1893],{"__ignoreMap":103},[21,1897,1898],{},"竞争线程可能先自旋尝试获得 Owner。仍然失败时，线程会进入等待结构并被挂起；持有线程退出后，Monitor 选择或唤醒后继线程继续竞争。",[21,1900,1901],{},"“重量级”的成本主要来自：",[55,1903,1904,1907,1910,1913],{},[58,1905,1906],{},"维护竞争和等待队列；",[58,1908,1909],{},"线程阻塞、唤醒与调度；",[58,1911,1912],{},"高竞争下的上下文切换；",[58,1914,1915],{},"对 Monitor 状态的原子协调。",[21,1917,1918],{},"它并不意味着每次操作都必然立刻执行一次昂贵的系统调用，HotSpot 仍包含快速路径和自适应自旋等优化。",[14,1920,1922],{"id":1921},"十四waitnotify-为什么会涉及重量级-monitor","十四、wait、notify 为什么会涉及重量级 Monitor",[21,1924,1925,1927],{},[25,1926,1524],{}," 的语义不是普通锁竞争：",[97,1929,1932],{"className":1930,"code":1931,"language":102,"meta":103},[100],"确认当前线程持有 Monitor\n完整释放锁及其重入层数\n进入 WaitSet\n等待 notify、notifyAll、中断或超时\n重新参与锁竞争\n恢复原重入状态后返回\n",[25,1933,1931],{"__ignoreMap":103},[21,1935,1936,1937,1939],{},"这需要 Owner、WaitSet、EntryList、重入次数等完整 Monitor 能力，因此 JDK 8 HotSpot 执行 ",[25,1938,1524],{}," 时会确保对象已经膨胀为 ObjectMonitor。",[21,1941,1942,356,1945,1948],{},[25,1943,1944],{},"notify()",[25,1946,1947],{},"notifyAll()"," 存在一个细节：如果对象当前只是由调用线程栈锁定，那么它不可能已经拥有 WaitSet，HotSpot 可以直接返回而不做无意义的膨胀；否则会取得或膨胀 ObjectMonitor，再处理等待线程。",[21,1950,1951,1953],{},[25,1952,1944],{}," 只是把等待线程从“等待条件”推进到“可以重新竞争锁”的阶段，并不会让它绕过当前 Owner 立即执行。",[14,1955,1957],{"id":1956},"十五identityhashcode-对锁状态的影响","十五、identityHashCode 对锁状态的影响",[21,1959,1960],{},"普通未锁定对象可以把 identity hash 保存在 Mark Word 中，但偏向锁的 Mark Word 需要保存 JavaThread 指针。",[21,1962,1963],{},"因此，对偏向对象计算：",[97,1965,1969],{"className":1966,"code":1967,"language":1968,"meta":103,"style":103},"language-java shiki shiki-themes github-light github-dark","System.identityHashCode(obj);\n","java",[25,1970,1971],{"__ignoreMap":103},[332,1972,1973],{"class":334,"line":335},[332,1974,1967],{},[21,1976,1977],{},"通常会导致偏向撤销，使对象头能够保存 hash。",[21,1979,1980],{},"如果对象正在使用轻量级锁，原 Mark Word 已经保存在当前线程栈的 Lock Record 中。为了稳定保存 identity hash，并让其他线程可靠读取，HotSpot 的相关路径可能把对象膨胀为 ObjectMonitor，再把 hash 保存在 Monitor 保存的 header 中。",[21,1982,1983],{},"所以观察锁升级实验时，不要在不知情的情况下调用可能计算 identity hash 的方法，否则实验本身会改变对象头。",[14,1985,1987],{"id":1986},"十六完整状态转换表","十六、完整状态转换表",[661,1989,1990,2003],{},[664,1991,1992],{},[667,1993,1994,1997,2000],{},[670,1995,1996],{},"当前状态",[670,1998,1999],{},"触发条件",[670,2001,2002],{},"可能结果",[683,2004,2005,2018,2031,2044,2056,2068,2080,2093,2105,2117,2129,2140,2153],{},[667,2006,2007,2012,2015],{},[688,2008,2009,2010],{},"普通未锁定 ",[25,2011,1319],{},[688,2013,2014],{},"首次进入同步块",[688,2016,2017],{},"CAS 成功后成为轻量级锁",[667,2019,2020,2025,2028],{},[688,2021,2022,2023],{},"匿名偏向 ",[25,2024,1340],{},[688,2026,2027],{},"第一个线程进入",[688,2029,2030],{},"CAS 写入线程指针，成为已偏向状态",[667,2032,2033,2038,2041],{},[688,2034,2035,2036],{},"已偏向 ",[25,2037,1340],{},[688,2039,2040],{},"原线程再次进入",[688,2042,2043],{},"保持偏向，低成本重入",[667,2045,2046,2050,2053],{},[688,2047,2035,2048],{},[25,2049,1340],{},[688,2051,2052],{},"新线程访问且 epoch 过期",[688,2054,2055],{},"尝试重偏向或撤销",[667,2057,2058,2062,2065],{},[688,2059,2035,2060],{},[25,2061,1340],{},[688,2063,2064],{},"新线程访问，原线程未持锁",[688,2066,2067],{},"撤销为普通状态，或按策略重偏向",[667,2069,2070,2074,2077],{},[688,2071,2035,2072],{},[25,2073,1340],{},[688,2075,2076],{},"新线程访问，原线程仍持锁",[688,2078,2079],{},"撤销偏向，转轻量级或膨胀",[667,2081,2082,2087,2090],{},[688,2083,2084,2085],{},"轻量级 ",[25,2086,1364],{},[688,2088,2089],{},"当前线程重入",[688,2091,2092],{},"新增递归 Lock Record，保持轻量级",[667,2094,2095,2099,2102],{},[688,2096,2084,2097],{},[25,2098,1364],{},[688,2100,2101],{},"无竞争退出",[688,2103,2104],{},"CAS 恢复原 Mark Word",[667,2106,2107,2111,2114],{},[688,2108,2084,2109],{},[25,2110,1364],{},[688,2112,2113],{},"竞争持续或恢复对象头失败",[688,2115,2116],{},"膨胀为 ObjectMonitor",[667,2118,2119,2122,2127],{},[688,2120,2121],{},"任意适用状态",[688,2123,2124,2126],{},[25,2125,1524],{}," 等需要完整 Monitor 语义",[688,2128,2116],{},[667,2130,2131,2134,2137],{},[688,2132,2133],{},"偏向\u002F轻量级",[688,2135,2136],{},"计算 identity hash 等特殊场景",[688,2138,2139],{},"撤销偏向或膨胀",[667,2141,2142,2147,2150],{},[688,2143,2144,2145],{},"重量级 ",[25,2146,1382],{},[688,2148,2149],{},"Owner 退出",[688,2151,2152],{},"唤醒\u002F选择后继；对象仍可保持膨胀",[667,2154,2155,2158,2161],{},[688,2156,2157],{},"空闲 ObjectMonitor",[688,2159,2160],{},"后续安全点清理",[688,2162,2163],{},"JVM 可能执行 Monitor deflation",[14,2165,2167],{"id":2166},"十七三条典型执行路径","十七、三条典型执行路径",[89,2169,2171],{"id":2170},"路径一同一个线程反复进入","路径一：同一个线程反复进入",[97,2173,2176],{"className":2174,"code":2175,"language":102,"meta":103},[100],"匿名偏向\n  → CAS 偏向线程 A\n  → A 执行同步块\n  → A 退出但保留偏向\n  → A 再次进入，快速命中\n",[25,2177,2175],{"__ignoreMap":103},[89,2179,2181],{"id":2180},"路径二线程交替使用没有重叠竞争","路径二：线程交替使用，没有重叠竞争",[21,2183,2184],{},"偏向关闭或偏向已撤销时：",[97,2186,2189],{"className":2187,"code":2188,"language":102,"meta":103},[100],"普通未锁定\n  → A 建立轻量级锁\n  → A CAS 恢复对象头\n  → B 建立轻量级锁\n  → B CAS 恢复对象头\n",[25,2190,2188],{"__ignoreMap":103},[21,2192,2193],{},"这种场景没有必要让线程进入阻塞等待。",[89,2195,2197],{"id":2196},"路径三多线程同时竞争","路径三：多线程同时竞争",[97,2199,2202],{"className":2200,"code":2201,"language":102,"meta":103},[100],"A 持有轻量级锁\n  → B CAS 失败并短暂自旋\n  → A 未及时释放\n  → 对象膨胀为 ObjectMonitor\n  → B 进入竞争\u002F等待结构\n  → A 退出并推进后继竞争\n",[25,2203,2201],{"__ignoreMap":103},[14,2205,2207],{"id":2206},"十八jol-验证时要注意什么","十八、JOL 验证时要注意什么",[21,2209,2210],{},"可以使用 JOL 查看对象布局：",[97,2212,2216],{"className":2213,"code":2214,"language":2215,"meta":103,"style":103},"language-xml shiki shiki-themes github-light github-dark","\u003Cdependency>\n    \u003CgroupId>org.openjdk.jol\u003C\u002FgroupId>\n    \u003CartifactId>jol-core\u003C\u002FartifactId>\n    \u003Cversion>0.17\u003C\u002Fversion>\n\u003C\u002Fdependency>\n","xml",[25,2217,2218,2223,2228,2233,2239],{"__ignoreMap":103},[332,2219,2220],{"class":334,"line":335},[332,2221,2222],{},"\u003Cdependency>\n",[332,2224,2225],{"class":334,"line":341},[332,2226,2227],{},"    \u003CgroupId>org.openjdk.jol\u003C\u002FgroupId>\n",[332,2229,2230],{"class":334,"line":347},[332,2231,2232],{},"    \u003CartifactId>jol-core\u003C\u002FartifactId>\n",[332,2234,2236],{"class":334,"line":2235},4,[332,2237,2238],{},"    \u003Cversion>0.17\u003C\u002Fversion>\n",[332,2240,2242],{"class":334,"line":2241},5,[332,2243,2244],{},"\u003C\u002Fdependency>\n",[21,2246,2247],{},"示例：",[97,2249,2251],{"className":1966,"code":2250,"language":1968,"meta":103,"style":103},"Object lock = new Object();\n\nSystem.out.println(ClassLayout.parseInstance(lock).toPrintable());\n\nsynchronized (lock) {\n    System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n}\n\nSystem.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[25,2252,2253,2258,2263,2268,2272,2277,2283,2289,2294],{"__ignoreMap":103},[332,2254,2255],{"class":334,"line":335},[332,2256,2257],{},"Object lock = new Object();\n",[332,2259,2260],{"class":334,"line":341},[332,2261,2262],{"emptyLinePlaceholder":1073},"\n",[332,2264,2265],{"class":334,"line":347},[332,2266,2267],{},"System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[332,2269,2270],{"class":334,"line":2235},[332,2271,2262],{"emptyLinePlaceholder":1073},[332,2273,2274],{"class":334,"line":2241},[332,2275,2276],{},"synchronized (lock) {\n",[332,2278,2280],{"class":334,"line":2279},6,[332,2281,2282],{},"    System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[332,2284,2286],{"class":334,"line":2285},7,[332,2287,2288],{},"}\n",[332,2290,2292],{"class":334,"line":2291},8,[332,2293,2262],{"emptyLinePlaceholder":1073},[332,2295,2297],{"class":334,"line":2296},9,[332,2298,2267],{},[21,2300,2301],{},"实验时要控制以下变量：",[55,2303,2304,2307,2313,2319,2322,2325],{},[58,2305,2306],{},"明确使用 JDK 8 HotSpot；",[58,2308,2309,2310,1286],{},"记录是否开启 ",[25,2311,2312],{},"UseBiasedLocking",[58,2314,2315,2316,1286],{},"记录 ",[25,2317,2318],{},"BiasedLockingStartupDelay",[58,2320,2321],{},"避免日志、调试器或工具意外计算 identity hash；",[58,2323,2324],{},"JOL 输出会受 32\u002F64 位、压缩指针和 GC 配置影响；",[58,2326,2327],{},"线程已经终止不代表任何场景都一定重偏向，仍要看 epoch 和撤销策略。",[21,2329,2330],{},"为了避免默认 4 秒延迟影响实验，可以显式设置：",[97,2332,2335],{"className":2333,"code":2334,"language":102,"meta":103},[100],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=0\n",[25,2336,2334],{"__ignoreMap":103},[21,2338,2339],{},"关闭偏向锁、直接观察轻量级路径：",[97,2341,2344],{"className":2342,"code":2343,"language":102,"meta":103},[100],"-XX:-UseBiasedLocking\n",[25,2345,2343],{"__ignoreMap":103},[14,2347,2349],{"id":2348},"十九常见误区","十九、常见误区",[89,2351,2353],{"id":2352},"误区一新对象一定先是无锁再升级为偏向锁","误区一：新对象一定先是无锁，再升级为偏向锁",[21,2355,2356],{},"不准确。偏向能力启用后，支持偏向的类可以让新对象直接使用匿名偏向的 prototype header。",[89,2358,2360],{"id":2359},"误区二第二个线程出现就一定升级为重量级锁","误区二：第二个线程出现就一定升级为重量级锁",[21,2362,2363],{},"不准确。它可能触发重偏向、偏向撤销、轻量级锁，也可能在存在实际竞争时膨胀。",[89,2365,2367],{"id":2366},"误区三轻量级锁就是一直自旋","误区三：轻量级锁就是一直自旋",[21,2369,2370],{},"不准确。轻量级锁的核心是栈上 Lock Record 与对象头 CAS；自旋是竞争时可能采用的等待优化，不是轻量级锁的完整定义。",[89,2372,2374],{"id":2373},"误区四重量级锁完全由操作系统-mutex-等同实现","误区四：重量级锁完全由操作系统 Mutex 等同实现",[21,2376,2377],{},"过度简化。ObjectMonitor 是 HotSpot 的 JVM 级同步结构，内部会使用 CAS、自旋、队列、park\u002Funpark 等机制；线程最终阻塞和唤醒会依赖操作系统能力，但两者不能直接画等号。",[89,2379,2381],{"id":2380},"误区五锁一旦膨胀这个对象终身都是重量级锁","误区五：锁一旦膨胀，这个对象终身都是重量级锁",[21,2383,2384],{},"不准确。当前竞争阶段通常不会立即降级，但空闲 Monitor 后续可能由 JVM 在安全点执行 deflation。",[89,2386,2388],{"id":2387},"误区六这套流程适用于所有-jdk","误区六：这套流程适用于所有 JDK",[21,2390,2391],{},"不准确。本文讨论的是 JDK 8 HotSpot。偏向锁后来被默认禁用，现代 HotSpot 的锁实现也持续演进，因此理解具体机制时必须先限定版本。",[14,2393,2395],{"id":2394},"二十参考资料","二十、参考资料",[55,2397,2398,2405,2412,2419,2426],{},[58,2399,2400],{},[73,2401,2404],{"href":2402,"rel":2403},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Foops\u002FmarkOop.hpp",[976],"JDK 8 HotSpot：markOop.hpp",[58,2406,2407],{},[73,2408,2411],{"href":2409,"rel":2410},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002Fsynchronizer.cpp",[976],"JDK 8 HotSpot：synchronizer.cpp",[58,2413,2414],{},[73,2415,2418],{"href":2416,"rel":2417},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FbiasedLocking.cpp",[976],"JDK 8 HotSpot：biasedLocking.cpp",[58,2420,2421],{},[73,2422,2425],{"href":2423,"rel":2424},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FobjectMonitor.hpp",[976],"JDK 8 HotSpot：objectMonitor.hpp",[58,2427,2428],{},[73,2429,2432],{"href":2430,"rel":2431},"https:\u002F\u002Fwww.cnblogs.com\u002Fstar95\u002Fp\u002F17542850.html",[976],"参考文章：浅析 synchronized 锁升级的原理与实现",[14,2434,2435],{"id":2435},"相关内容",[55,2437,2438],{},[58,2439,2440],{},[73,2441,2443],{"href":2442},"\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock\u002F","synchronized 和 ReentrantLock 的区别",[1021,2445,1023],{},{"title":103,"searchDepth":341,"depth":341,"links":2447},[2448,2449,2450,2454,2459,2463,2467,2472,2476,2479,2480,2481,2482,2483,2484,2485,2486,2491,2492,2500,2501],{"id":1089,"depth":341,"text":1090},{"id":1150,"depth":341,"text":1151},{"id":1233,"depth":341,"text":1234,"children":2451},[2452,2453],{"id":1269,"depth":347,"text":1270},{"id":1436,"depth":347,"text":1437},{"id":1498,"depth":341,"text":1499,"children":2455},[2456,2457,2458],{"id":1505,"depth":347,"text":1505},{"id":1511,"depth":347,"text":1512},{"id":1518,"depth":347,"text":1518},{"id":1536,"depth":341,"text":1537,"children":2460},[2461,2462],{"id":1552,"depth":347,"text":1552},{"id":1567,"depth":347,"text":1567},{"id":1594,"depth":341,"text":1595,"children":2464},[2465,2466],{"id":1632,"depth":347,"text":1632},{"id":1647,"depth":347,"text":1647},{"id":1653,"depth":341,"text":1654,"children":2468},[2469,2470,2471],{"id":1663,"depth":347,"text":1664},{"id":1670,"depth":347,"text":1671},{"id":1677,"depth":347,"text":1678},{"id":1692,"depth":341,"text":1693,"children":2473},[2474,2475],{"id":1458,"depth":347,"text":1458},{"id":1713,"depth":347,"text":1713},{"id":1722,"depth":341,"text":1723,"children":2477},[2478],{"id":1770,"depth":347,"text":1771},{"id":1777,"depth":341,"text":1778},{"id":1802,"depth":341,"text":1803},{"id":1830,"depth":341,"text":1831},{"id":1872,"depth":341,"text":1873},{"id":1921,"depth":341,"text":1922},{"id":1956,"depth":341,"text":1957},{"id":1986,"depth":341,"text":1987},{"id":2166,"depth":341,"text":2167,"children":2487},[2488,2489,2490],{"id":2170,"depth":347,"text":2171},{"id":2180,"depth":347,"text":2181},{"id":2196,"depth":347,"text":2197},{"id":2206,"depth":341,"text":2207},{"id":2348,"depth":341,"text":2349,"children":2493},[2494,2495,2496,2497,2498,2499],{"id":2352,"depth":347,"text":2353},{"id":2359,"depth":347,"text":2360},{"id":2366,"depth":347,"text":2367},{"id":2373,"depth":347,"text":2374},{"id":2380,"depth":347,"text":2381},{"id":2387,"depth":347,"text":2388},{"id":2394,"depth":341,"text":2395},{"id":2435,"depth":341,"text":2435},"wiki:java:jdk8-synchronized-lock-states","从 Mark Word、Lock Record 与 ObjectMonitor 出发，系统理解 JDK 8 HotSpot 中偏向锁、轻量级锁、重量级锁的获取、撤销、膨胀和释放过程。",{},35,"\u002Fjava\u002Fjdk8-synchronized-lock-states","并发编程",{"title":1084,"description":2503},"java\u002Fjdk8-synchronized-lock-states","2026-07-23","gORBab_xEnsWFF1EDOBMo_opFfosXf7qG31N3VQ4jOM",{"id":2513,"title":2443,"body":2514,"commentId":3409,"description":3410,"difficulty":3411,"draft":1070,"extension":1071,"meta":3412,"navigation":1073,"order":3413,"path":3414,"section":1076,"seo":3415,"stem":3416,"updated":3417,"__hash__":3418},"java\u002Fjava\u002Fsynchronized-vs-reentrantlock.md",{"type":7,"value":2515,"toc":3388},[2516,2519,2522,2527,2530,2537,2539,2547,2557,2561,2563,2569,2572,2578,2581,2584,2590,2597,2600,2604,2757,2759,2763,2766,2771,2774,2780,2783,2786,2792,2795,2797,2801,2807,2810,2813,2819,2822,2827,2829,2833,2836,2839,2845,2848,2851,2857,2860,2866,2868,2874,2877,2879,2883,2886,2892,2895,2901,2906,2909,2911,2915,2920,2923,2929,2935,2941,2944,2950,2953,2959,2962,2968,2971,2977,2980,2986,2989,2991,2995,2998,3001,3007,3010,3016,3019,3022,3025,3031,3034,3037,3043,3046,3052,3059,3061,3065,3070,3076,3079,3082,3088,3095,3101,3104,3110,3113,3115,3119,3122,3125,3141,3144,3149,3152,3154,3158,3161,3181,3183,3189,3192,3197,3199,3203,3205,3231,3234,3240,3243,3245,3249,3294,3297,3302,3309,3315,3318,3320,3324,3328,3331,3335,3338,3342,3345,3353,3357,3360,3364,3367,3370,3376,3378,3381],[21,2517,2518],{},"synchronized和reentrantLock的区别？",[10,2520,2521],{"id":2521},"面试标准回答",[18,2523,2524],{},[21,2525,2526],{},"synchronized 是 JVM 层面的内置锁，语法简单，进入同步块后自动加锁，退出时自动释放，支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁，同样支持可重入，但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据；功能简单时优先 synchronized，需要精细控制时使用 ReentrantLock，并且必须在 finally 中释放锁。",[21,2528,2529],{},"一句话记忆：",[18,2531,2532],{},[21,2533,2534],{},[45,2535,2536],{},"synchronized 简单自动，ReentrantLock 灵活可控。",[10,2538,50],{"id":50},[21,2540,2541,356,2543,2546],{},[25,2542,1095],{},[25,2544,2545],{},"ReentrantLock"," 都能实现互斥和可见性，但定位不同：",[18,2548,2549],{},[21,2550,2551,2553,2554,2556],{},[25,2552,1095],{}," 是 JVM 原生关键字，语法简单；",[25,2555,2545],{}," 是 JUC 提供的显式锁，功能更丰富、控制能力更强。",[14,2558,2560],{"id":2559},"一基本用法","一、基本用法",[89,2562,1095],{"id":1095},[97,2564,2567],{"className":2565,"code":2566,"language":102},[100],"synchronized (lock) {\n    \u002F\u002F 临界区\n}\n",[25,2568,2566],{"__ignoreMap":103},[21,2570,2571],{},"或者：",[97,2573,2576],{"className":2574,"code":2575,"language":102},[100],"public synchronized void method() {\n}\n",[25,2577,2575],{"__ignoreMap":103},[21,2579,2580],{},"锁会自动释放。",[89,2582,2545],{"id":2583},"reentrantlock",[97,2585,2588],{"className":2586,"code":2587,"language":102},[100],"ReentrantLock lock = new ReentrantLock();\n\nlock.lock();\ntry {\n    \u002F\u002F 临界区\n} finally {\n    lock.unlock();\n}\n",[25,2589,2587],{"__ignoreMap":103},[21,2591,2592,2593,2596],{},"必须手动释放，所以通常必须写在 ",[25,2594,2595],{},"finally"," 中。",[2598,2599],"hr",{},[14,2601,2603],{"id":2602},"二核心区别","二、核心区别",[661,2605,2606,2617],{},[664,2607,2608],{},[667,2609,2610,2613,2615],{},[670,2611,2612],{},"对比项",[670,2614,1095],{},[670,2616,2545],{},[683,2618,2619,2630,2641,2651,2661,2674,2686,2698,2709,2724,2735,2746],{},[667,2620,2621,2624,2627],{},[688,2622,2623],{},"实现层次",[688,2625,2626],{},"JVM 关键字、Monitor",[688,2628,2629],{},"Java 类，基于 AQS",[667,2631,2632,2635,2638],{},[688,2633,2634],{},"加锁释放",[688,2636,2637],{},"自动",[688,2639,2640],{},"手动",[667,2642,2643,2646,2649],{},[688,2644,2645],{},"可重入",[688,2647,2648],{},"支持",[688,2650,2648],{},[667,2652,2653,2656,2659],{},[688,2654,2655],{},"公平锁",[688,2657,2658],{},"不支持显式配置",[688,2660,2648],{},[667,2662,2663,2666,2669],{},[688,2664,2665],{},"可中断获取锁",[688,2667,2668],{},"不支持",[688,2670,2671],{},[25,2672,2673],{},"lockInterruptibly()",[667,2675,2676,2679,2681],{},[688,2677,2678],{},"尝试获取锁",[688,2680,2668],{},[688,2682,2683],{},[25,2684,2685],{},"tryLock()",[667,2687,2688,2691,2693],{},[688,2689,2690],{},"超时获取锁",[688,2692,2668],{},[688,2694,2695],{},[25,2696,2697],{},"tryLock(timeout, unit)",[667,2699,2700,2703,2706],{},[688,2701,2702],{},"条件队列",[688,2704,2705],{},"一个 Monitor WaitSet",[688,2707,2708],{},"可创建多个 Condition",[667,2710,2711,2714,2719],{},[688,2712,2713],{},"等待通知",[688,2715,2716],{},[25,2717,2718],{},"wait\u002Fnotify\u002FnotifyAll",[688,2720,2721],{},[25,2722,2723],{},"await\u002Fsignal\u002FsignalAll",[667,2725,2726,2729,2732],{},[688,2727,2728],{},"锁状态查询",[688,2730,2731],{},"能力有限",[688,2733,2734],{},"提供较多查询方法",[667,2736,2737,2740,2743],{},[688,2738,2739],{},"编码复杂度",[688,2741,2742],{},"低",[688,2744,2745],{},"较高",[667,2747,2748,2751,2754],{},[688,2749,2750],{},"异常释放",[688,2752,2753],{},"自动释放",[688,2755,2756],{},"必须 finally unlock",[2598,2758],{},[10,2760,2762],{"id":2761},"三两者都支持可重入","三、两者都支持可重入",[21,2764,2765],{},"可重入指：",[18,2767,2768],{},[21,2769,2770],{},"同一个线程已经持有锁时，可以再次获取同一把锁。",[89,2772,1095],{"id":2773},"synchronized-1",[97,2775,2778],{"className":2776,"code":2777,"language":102},[100],"public synchronized void methodA() {\n    methodB();\n}\n\npublic synchronized void methodB() {\n}\n",[25,2779,2777],{"__ignoreMap":103},[21,2781,2782],{},"同一个对象上的两个同步方法，线程可以重入。",[89,2784,2545],{"id":2785},"reentrantlock-1",[97,2787,2790],{"className":2788,"code":2789,"language":102},[100],"lock.lock();\ntry {\n    lock.lock();\n    try {\n        \u002F\u002F 重入\n    } finally {\n        lock.unlock();\n    }\n} finally {\n    lock.unlock();\n}\n",[25,2791,2789],{"__ignoreMap":103},[21,2793,2794],{},"获取几次，就必须释放几次。",[2598,2796],{},[10,2798,2800],{"id":2799},"四reentrantlock-支持公平锁","四、ReentrantLock 支持公平锁",[97,2802,2805],{"className":2803,"code":2804,"language":102},[100],"ReentrantLock fairLock = new ReentrantLock(true);\n",[25,2806,2804],{"__ignoreMap":103},[21,2808,2809],{},"公平锁会尽量按 AQS 队列顺序获取锁。",[21,2811,2812],{},"默认是非公平锁：",[97,2814,2817],{"className":2815,"code":2816,"language":102},[100],"ReentrantLock lock = new ReentrantLock();\n",[25,2818,2816],{"__ignoreMap":103},[21,2820,2821],{},"非公平锁允许新线程插队，吞吐量通常更高。",[21,2823,2824,2826],{},[25,2825,1095],{}," 没有 API 让你指定公平性，通常按非公平竞争理解。",[2598,2828],{},[10,2830,2832],{"id":2831},"五reentrantlock-支持可响应中断等待","五、ReentrantLock 支持可响应中断等待",[21,2834,2835],{},"假设线程正在等待锁。",[21,2837,2838],{},"使用：",[97,2840,2843],{"className":2841,"code":2842,"language":102},[100],"lock.lock();\n",[25,2844,2842],{"__ignoreMap":103},[21,2846,2847],{},"即使线程被中断，也不会因为中断立即退出获取锁过程。",[21,2849,2850],{},"而：",[97,2852,2855],{"className":2853,"code":2854,"language":102},[100],"lock.lockInterruptibly();\n",[25,2856,2854],{"__ignoreMap":103},[21,2858,2859],{},"等待期间如果收到中断，会抛出：",[97,2861,2864],{"className":2862,"code":2863,"language":102},[100],"InterruptedException\n",[25,2865,2863],{"__ignoreMap":103},[21,2867,149],{},[97,2869,2872],{"className":2870,"code":2871,"language":102},[100],"try {\n    lock.lockInterruptibly();\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} catch (InterruptedException e) {\n    Thread.currentThread().interrupt();\n}\n",[25,2873,2871],{"__ignoreMap":103},[21,2875,2876],{},"这对于避免线程长时间死等、响应取消请求很有价值。",[2598,2878],{},[10,2880,2882],{"id":2881},"六reentrantlock-支持尝试获取和超时","六、ReentrantLock 支持尝试获取和超时",[89,2884,2885],{"id":2885},"立即尝试",[97,2887,2890],{"className":2888,"code":2889,"language":102},[100],"if (lock.tryLock()) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 获取失败，走降级逻辑\n}\n",[25,2891,2889],{"__ignoreMap":103},[89,2893,2894],{"id":2894},"超时尝试",[97,2896,2899],{"className":2897,"code":2898,"language":102},[100],"if (lock.tryLock(2, TimeUnit.SECONDS)) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 两秒内未拿到锁\n}\n",[25,2900,2898],{"__ignoreMap":103},[21,2902,2903,2905],{},[25,2904,1095],{}," 一旦进入竞争，只能等待，无法直接设置超时。",[21,2907,2908],{},"这也是 ReentrantLock 在需要降级、超时控制时的重要优势。",[2598,2910],{},[10,2912,2914],{"id":2913},"七condition-比-waitnotify-更灵活","七、Condition 比 wait\u002Fnotify 更灵活",[21,2916,2917,2919],{},[25,2918,1095],{}," 的一个 Monitor 只有一个 WaitSet。",[21,2921,2922],{},"例如阻塞队列里：",[97,2924,2927],{"className":2925,"code":2926,"language":102},[100],"生产者等待 notFull\n消费者等待 notEmpty\n",[25,2928,2926],{"__ignoreMap":103},[21,2930,2931,2932,2934],{},"使用 ",[25,2933,1095],{}," 时，生产者和消费者都在同一个 WaitSet 中，通常要：",[97,2936,2939],{"className":2937,"code":2938,"language":102},[100],"notifyAll();\n",[25,2940,2938],{"__ignoreMap":103},[21,2942,2943],{},"而 ReentrantLock 可以创建多个 Condition：",[97,2945,2948],{"className":2946,"code":2947,"language":102},[100],"Condition notFull = lock.newCondition();\nCondition notEmpty = lock.newCondition();\n",[25,2949,2947],{"__ignoreMap":103},[21,2951,2952],{},"生产者等待：",[97,2954,2957],{"className":2955,"code":2956,"language":102},[100],"while (queue.size() == capacity) {\n    notFull.await();\n}\n",[25,2958,2956],{"__ignoreMap":103},[21,2960,2961],{},"消费者等待：",[97,2963,2966],{"className":2964,"code":2965,"language":102},[100],"while (queue.isEmpty()) {\n    notEmpty.await();\n}\n",[25,2967,2965],{"__ignoreMap":103},[21,2969,2970],{},"生产完成：",[97,2972,2975],{"className":2973,"code":2974,"language":102},[100],"notEmpty.signal();\n",[25,2976,2974],{"__ignoreMap":103},[21,2978,2979],{},"消费完成：",[97,2981,2984],{"className":2982,"code":2983,"language":102},[100],"notFull.signal();\n",[25,2985,2983],{"__ignoreMap":103},[21,2987,2988],{},"这样可以精确唤醒，减少无效唤醒。",[2598,2990],{},[10,2992,2994],{"id":2993},"八底层实现不同","八、底层实现不同",[14,2996,1095],{"id":2997},"synchronized-2",[21,2999,3000],{},"经典理解：",[97,3002,3005],{"className":3003,"code":3004,"language":102},[100],"对象头 Mark Word\n+\nMonitor\n+\nJVM 锁优化\n",[25,3006,3004],{"__ignoreMap":103},[21,3008,3009],{},"JDK 8 中可能经历：",[97,3011,3014],{"className":3012,"code":3013,"language":102},[100],"偏向锁\n→ 轻量级锁\n→ 重量级锁\n",[25,3015,3013],{"__ignoreMap":103},[21,3017,3018],{},"这是 HotSpot 的 JVM 实现优化。",[14,3020,2545],{"id":3021},"reentrantlock-2",[21,3023,3024],{},"主要基于：",[97,3026,3029],{"className":3027,"code":3028,"language":102},[100],"AbstractQueuedSynchronizer\n",[25,3030,3028],{"__ignoreMap":103},[21,3032,3033],{},"也就是 AQS。",[21,3035,3036],{},"核心状态：",[97,3038,3041],{"className":3039,"code":3040,"language":102},[100],"volatile int state;\n",[25,3042,3040],{"__ignoreMap":103},[21,3044,3045],{},"独占锁时：",[97,3047,3050],{"className":3048,"code":3049,"language":102},[100],"state = 0  未加锁\nstate = 1  第一次获取\nstate > 1  重入次数\n",[25,3051,3049],{"__ignoreMap":103},[21,3053,3054,3055,3058],{},"竞争失败的线程进入 CLH 变体同步队列，并通过 ",[25,3056,3057],{},"park\u002Funpark"," 等待和唤醒。",[2598,3060],{},[10,3062,3064],{"id":3063},"九异常情况下的差异","九、异常情况下的差异",[21,3066,3067,3069],{},[25,3068,1095],{},"：",[97,3071,3074],{"className":3072,"code":3073,"language":102},[100],"synchronized (lock) {\n    throw new RuntimeException();\n}\n",[25,3075,3073],{"__ignoreMap":103},[21,3077,3078],{},"离开同步块时，JVM 自动释放锁。",[21,3080,3081],{},"ReentrantLock：",[97,3083,3086],{"className":3084,"code":3085,"language":102},[100],"lock.lock();\ndoWork();\nlock.unlock();\n",[25,3087,3085],{"__ignoreMap":103},[21,3089,3090,3091,3094],{},"如果 ",[25,3092,3093],{},"doWork()"," 抛异常：",[97,3096,3099],{"className":3097,"code":3098,"language":102},[100],"unlock 没执行\n→ 锁永久未释放\n",[25,3100,3098],{"__ignoreMap":103},[21,3102,3103],{},"所以必须写：",[97,3105,3108],{"className":3106,"code":3107,"language":102},[100],"lock.lock();\ntry {\n    doWork();\n} finally {\n    lock.unlock();\n}\n",[25,3109,3107],{"__ignoreMap":103},[21,3111,3112],{},"这是 ReentrantLock 最常见的编码风险。",[2598,3114],{},[10,3116,3118],{"id":3117},"十性能区别","十、性能区别",[21,3120,3121],{},"早期 JDK 中，ReentrantLock 性能常常明显优于 synchronized。",[21,3123,3124],{},"但 JDK 6 以后，JVM 对 synchronized 做了大量优化：",[55,3126,3127,3130,3132,3135,3138],{},[58,3128,3129],{},"偏向锁",[58,3131,1367],{},[58,3133,3134],{},"自旋",[58,3136,3137],{},"锁消除",[58,3139,3140],{},"锁粗化",[21,3142,3143],{},"现代 JDK 中：",[18,3145,3146],{},[21,3147,3148],{},"两者性能差异通常不是选型的首要依据。",[21,3150,3151],{},"应该根据功能需求选择，而不是简单认为 ReentrantLock 一定更快。",[2598,3153],{},[10,3155,3157],{"id":3156},"十一什么时候使用-synchronized","十一、什么时候使用 synchronized",[21,3159,3160],{},"适合：",[55,3162,3163,3166,3169,3172,3175,3178],{},[58,3164,3165],{},"临界区简单",[58,3167,3168],{},"不需要公平锁",[58,3170,3171],{},"不需要超时获取",[58,3173,3174],{},"不需要中断锁等待",[58,3176,3177],{},"只有一个等待条件",[58,3179,3180],{},"希望代码简单、自动释放锁",[21,3182,149],{},[97,3184,3187],{"className":3185,"code":3186,"language":102},[100],"public synchronized void increment() {\n    count++;\n}\n",[25,3188,3186],{"__ignoreMap":103},[21,3190,3191],{},"一般优先原则：",[18,3193,3194],{},[21,3195,3196],{},"能用 synchronized 清晰表达时，优先用 synchronized。",[2598,3198],{},[10,3200,3202],{"id":3201},"十二什么时候使用-reentrantlock","十二、什么时候使用 ReentrantLock",[21,3204,3160],{},[55,3206,3207,3210,3216,3219,3222,3225,3228],{},[58,3208,3209],{},"需要公平锁",[58,3211,3212,3213],{},"需要 ",[25,3214,3215],{},"tryLock",[58,3217,3218],{},"需要超时获取锁",[58,3220,3221],{},"需要可中断等待",[58,3223,3224],{},"需要多个 Condition",[58,3226,3227],{},"需要查询等待队列、持锁状态",[58,3229,3230],{},"需要更精细的锁控制",[21,3232,3233],{},"例如转账时避免死锁：",[97,3235,3238],{"className":3236,"code":3237,"language":102},[100],"if (accountA.lock.tryLock(100, TimeUnit.MILLISECONDS)) {\n    try {\n        if (accountB.lock.tryLock(100, TimeUnit.MILLISECONDS)) {\n            try {\n                transfer();\n            } finally {\n                accountB.lock.unlock();\n            }\n        }\n    } finally {\n        accountA.lock.unlock();\n    }\n}\n",[25,3239,3237],{"__ignoreMap":103},[21,3241,3242],{},"使用 synchronized 很难实现这种超时退避。",[2598,3244],{},[10,3246,3248],{"id":3247},"十三waitnotify-和-awaitsignal-对应关系","十三、wait\u002Fnotify 和 await\u002Fsignal 对应关系",[661,3250,3251,3259],{},[664,3252,3253],{},[667,3254,3255,3257],{},[670,3256,1095],{},[670,3258,2545],{},[683,3260,3261,3272,3283],{},[667,3262,3263,3267],{},[688,3264,3265],{},[25,3266,1524],{},[688,3268,3269],{},[25,3270,3271],{},"Condition.await()",[667,3273,3274,3278],{},[688,3275,3276],{},[25,3277,1944],{},[688,3279,3280],{},[25,3281,3282],{},"Condition.signal()",[667,3284,3285,3289],{},[688,3286,3287],{},[25,3288,1947],{},[688,3290,3291],{},[25,3292,3293],{},"Condition.signalAll()",[21,3295,3296],{},"两者都要求：",[18,3298,3299],{},[21,3300,3301],{},"调用等待或通知方法前，必须先持有对应的锁。",[21,3303,3304,3305,3308],{},"并且等待都应该放在 ",[25,3306,3307],{},"while"," 中：",[97,3310,3313],{"className":3311,"code":3312,"language":102},[100],"while (!conditionSatisfied()) {\n    condition.await();\n}\n",[25,3314,3312],{"__ignoreMap":103},[21,3316,3317],{},"防止虚假唤醒和竞争后条件失效。",[2598,3319],{},[10,3321,3323],{"id":3322},"十四常见误区","十四、常见误区",[89,3325,3327],{"id":3326},"误区一reentrantlock-才支持可重入","误区一：ReentrantLock 才支持可重入",[21,3329,3330],{},"不对。两者都可重入。",[89,3332,3334],{"id":3333},"误区二reentrantlock-一定比-synchronized-快","误区二：ReentrantLock 一定比 synchronized 快",[21,3336,3337],{},"不对。现代 JVM 下要看场景，功能差异比纯性能更重要。",[89,3339,3341],{"id":3340},"误区三synchronized-会自动释放reentrantlock-不会","误区三：synchronized 会自动释放，ReentrantLock 不会",[21,3343,3344],{},"更准确地说：",[55,3346,3347,3350],{},[58,3348,3349],{},"synchronized 离开同步块时自动释放",[58,3351,3352],{},"ReentrantLock 必须显式 unlock",[89,3354,3356],{"id":3355},"误区四公平锁一定更好","误区四：公平锁一定更好",[21,3358,3359],{},"公平锁吞吐量通常更低，只在有明确公平性需求时使用。",[89,3361,3363],{"id":3362},"误区五trylock-失败后可以直接-unlock","误区五：tryLock 失败后可以直接 unlock",[21,3365,3366],{},"不可以。只有成功获取锁后才能释放。",[21,3368,3369],{},"正确写法：",[97,3371,3374],{"className":3372,"code":3373,"language":102},[100],"boolean locked = false;\ntry {\n    locked = lock.tryLock();\n    if (!locked) {\n        return;\n    }\n\n    doWork();\n} finally {\n    if (locked) {\n        lock.unlock();\n    }\n}\n",[25,3375,3373],{"__ignoreMap":103},[2598,3377],{},[14,3379,3380],{"id":3380},"相关主题",[55,3382,3383],{},[58,3384,3385],{},[73,3386,1084],{"href":3387},"\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-lock-states\u002F",{"title":103,"searchDepth":341,"depth":341,"links":3389},[3390,3394,3400,3401,3408],{"id":2559,"depth":341,"text":2560,"children":3391},[3392,3393],{"id":1095,"depth":347,"text":1095},{"id":2583,"depth":347,"text":2545},{"id":2602,"depth":341,"text":2603,"children":3395},[3396,3397,3398,3399],{"id":2773,"depth":347,"text":1095},{"id":2785,"depth":347,"text":2545},{"id":2885,"depth":347,"text":2885},{"id":2894,"depth":347,"text":2894},{"id":2997,"depth":341,"text":1095},{"id":3021,"depth":341,"text":2545,"children":3402},[3403,3404,3405,3406,3407],{"id":3326,"depth":347,"text":3327},{"id":3333,"depth":347,"text":3334},{"id":3340,"depth":347,"text":3341},{"id":3355,"depth":347,"text":3356},{"id":3362,"depth":347,"text":3363},{"id":3380,"depth":341,"text":3380},"wiki:java:synchronized-vs-reentrantlock","对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。","intermediate",{},71,"\u002Fjava\u002Fsynchronized-vs-reentrantlock",{"title":2443,"description":3410},"java\u002Fsynchronized-vs-reentrantlock","2026-07-21","vNahPOMAudgb7Mu0G7t_sn5sjTaBeUGcX25bRe10EXg",{"id":3420,"title":3421,"body":3422,"commentId":4578,"description":4579,"difficulty":1069,"draft":1070,"extension":1071,"meta":4580,"navigation":1073,"order":4581,"path":4582,"section":1076,"seo":4583,"stem":4584,"updated":1079,"__hash__":4585},"java\u002Fjava\u002Fkafka-message-loss-prevention.md","Kafka 如何保证消息不丢失",{"type":7,"value":3423,"toc":4534},[3424,3427,3429,3445,3447,3454,3456,3459,3466,3469,3478,3482,3485,3499,3502,3506,3512,3518,3573,3576,3585,3593,3597,3600,3629,3632,3635,3649,3653,3656,3665,3672,3707,3714,3718,3721,3727,3730,3733,3739,3742,3746,3750,3753,3759,3762,3765,3769,3772,3775,3781,3784,3790,3795,3801,3804,3809,3812,3817,3820,3824,3833,3839,3842,3851,3854,3860,3869,3874,3877,3881,3885,3894,3897,3903,3906,3910,3913,3919,3922,3968,3971,3975,3978,3984,3987,4004,4011,4015,4018,4024,4027,4036,4039,4042,4046,4049,4055,4059,4062,4065,4071,4074,4077,4097,4104,4107,4113,4119,4123,4126,4128,4134,4140,4143,4157,4161,4232,4235,4242,4246,4296,4299,4306,4309,4313,4423,4427,4434,4442,4446,4452,4456,4459,4463,4466,4470,4473,4477,4480,4499,4502,4504,4532],[10,3425,3421],{"id":3426},"kafka-如何保证消息不丢失",[14,3428,16],{"id":16},[18,3430,3431],{},[21,3432,3433,3434,3436,3437,3440,3441,3444],{},"Kafka 保证消息不丢失需要覆盖生产者、Broker 和消费者三段链路。生产者侧使用 ",[25,3435,31],{},"、开启重试和幂等生产，并检查发送回调，对最终失败进行落库、告警或补偿；Broker 侧通常设置三副本、",[25,3438,3439],{},"min.insync.replicas=2","，并关闭非同步副本选举，即配置 ",[25,3442,3443],{},"unclean.leader.election.enable=false","，保证消息在足够多的同步副本写入后才确认成功；消费者侧关闭自动提交，在业务处理成功后再提交 Offset，失败时不提交以便重新消费。这样通常实现至少一次投递，所以消费端还必须通过事件 ID、唯一索引或状态机保证幂等。涉及数据库与 Kafka 的一致性时，可以使用 Transactional Outbox；消费端需要应对重复消息时，可以使用 Inbox。Kafka 的消息不丢失是端到端保障，不是只配置一个参数。",[21,3446,39],{},[18,3448,3449],{},[21,3450,3451],{},[45,3452,3453],{},"生产者确认成功，Broker 多副本可靠保存，消费者处理成功后提交，业务端用幂等和补偿兜底。",[14,3455,50],{"id":50},[21,3457,3458],{},"Kafka 消息不丢失不是靠某一个参数实现的，而是一个端到端问题：",[18,3460,3461],{},[21,3462,3463],{},[45,3464,3465],{},"生产者必须确认消息发送成功，Broker 必须把消息可靠地保存在足够多的副本中，消费者必须在业务处理成功后再提交 Offset。",[21,3467,3468],{},"其中任何一个环节处理不当，都可能出现“业务上看起来消息丢了”的情况。",[21,3470,3471],{},[73,3472,3475],{"href":3473,"target":76,"rel":3474},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-loss-prevention.svg",[78,79],[81,3476],{"src":3473,"alt":3477},"Kafka 消息不丢失的端到端保障链路",[14,3479,3481],{"id":3480},"一先明确什么叫消息丢失","一、先明确什么叫“消息丢失”",[21,3483,3484],{},"常见的丢失场景主要有四类：",[1616,3486,3487,3490,3493,3496],{},[58,3488,3489],{},"生产者调用发送方法后没有检查结果，实际发送失败却被业务忽略。",[58,3491,3492],{},"Leader 收到消息后就返回成功，消息还未复制到 Follower，Leader 随后故障。",[58,3494,3495],{},"消费者先提交 Offset，再执行业务；业务处理失败后，消息不会再次投递。",[58,3497,3498],{},"Kafka 中的消息没有丢，但写数据库、调用下游等业务操作失败，又没有补偿机制。",[21,3500,3501],{},"因此，Kafka 中仍能查到消息，不代表业务一定处理成功；反过来，业务没有结果，也不一定是 Broker 丢了消息。",[14,3503,3505],{"id":3504},"二生产者如何避免丢消息","二、生产者如何避免丢消息",[89,3507,3509,3510],{"id":3508},"_1-使用-acksall","1. 使用 ",[25,3511,31],{},[21,3513,3514,3515,3517],{},"生产者的 ",[25,3516,566],{}," 决定 Broker 在什么条件下确认发送成功：",[661,3519,3520,3533],{},[664,3521,3522],{},[667,3523,3524,3527,3530],{},[670,3525,3526],{},"配置",[670,3528,3529],{},"确认时机",[670,3531,3532],{},"风险",[683,3534,3535,3548,3561],{},[667,3536,3537,3542,3545],{},[688,3538,3539],{},[25,3540,3541],{},"acks=0",[688,3543,3544],{},"不等待 Broker 响应",[688,3546,3547],{},"发送失败也无法感知，风险最高",[667,3549,3550,3555,3558],{},[688,3551,3552],{},[25,3553,3554],{},"acks=1",[688,3556,3557],{},"Leader 写入本地日志后返回",[688,3559,3560],{},"Leader 故障且副本尚未同步时可能丢失",[667,3562,3563,3567,3570],{},[688,3564,3565],{},[25,3566,31],{},[688,3568,3569],{},"当前 ISR 中的副本都确认后返回",[688,3571,3572],{},"Kafka 能提供的最强生产者确认保证",[21,3574,3575],{},"关键配置：",[97,3577,3579],{"className":326,"code":3578,"language":328,"meta":103,"style":103},"acks=all\n",[25,3580,3581],{"__ignoreMap":103},[332,3582,3583],{"class":334,"line":335},[332,3584,3578],{},[21,3586,3587,3589,3590,3592],{},[25,3588,31],{}," 并不等于绝对不丢。它还需要与副本数和 ",[25,3591,35],{}," 配合，否则 ISR 中只剩 Leader 一个副本时，依然可能成功写入。",[89,3594,3596],{"id":3595},"_2-开启重试和幂等生产","2. 开启重试和幂等生产",[21,3598,3599],{},"网络抖动、Leader 切换等临时故障可能导致发送失败，生产者需要允许重试：",[97,3601,3603],{"className":326,"code":3602,"language":328,"meta":103,"style":103},"enable.idempotence=true\nacks=all\nretries=2147483647\nmax.in.flight.requests.per.connection=5\ndelivery.timeout.ms=120000\n",[25,3604,3605,3610,3614,3619,3624],{"__ignoreMap":103},[332,3606,3607],{"class":334,"line":335},[332,3608,3609],{},"enable.idempotence=true\n",[332,3611,3612],{"class":334,"line":341},[332,3613,3578],{},[332,3615,3616],{"class":334,"line":347},[332,3617,3618],{},"retries=2147483647\n",[332,3620,3621],{"class":334,"line":2235},[332,3622,3623],{},"max.in.flight.requests.per.connection=5\n",[332,3625,3626],{"class":334,"line":2241},[332,3627,3628],{},"delivery.timeout.ms=120000\n",[21,3630,3631],{},"幂等生产者会给消息附加 Producer ID 和序列号，使 Broker 能识别同一生产会话内的重复写入，避免因重试产生重复消息。",[21,3633,3634],{},"需要注意：",[55,3636,3637,3640,3646],{},[58,3638,3639],{},"幂等生产解决的是重试导致的重复写入，不是发送失败后的业务补偿。",[58,3641,3642,3645],{},[25,3643,3644],{},"delivery.timeout.ms"," 到期后，生产者仍可能最终失败。",[58,3647,3648],{},"业务不能无限依赖客户端重试，最终失败必须记录、告警或进入补偿流程。",[89,3650,3652],{"id":3651},"_3-必须检查发送结果","3. 必须检查发送结果",[21,3654,3655],{},"下面这种“只发送、不处理结果”的写法存在风险：",[97,3657,3659],{"className":1966,"code":3658,"language":1968,"meta":103,"style":103},"producer.send(record);\n",[25,3660,3661],{"__ignoreMap":103},[332,3662,3663],{"class":334,"line":335},[332,3664,3658],{},[21,3666,3667,3668,3671],{},"应该检查回调或 ",[25,3669,3670],{},"Future"," 的执行结果：",[97,3673,3675],{"className":1966,"code":3674,"language":1968,"meta":103,"style":103},"producer.send(record, (metadata, exception) -> {\n    if (exception != null) {\n        \u002F\u002F 记录原始消息、告警并进入补偿流程\n        handleSendFailure(record, exception);\n    }\n});\n",[25,3676,3677,3682,3687,3692,3697,3702],{"__ignoreMap":103},[332,3678,3679],{"class":334,"line":335},[332,3680,3681],{},"producer.send(record, (metadata, exception) -> {\n",[332,3683,3684],{"class":334,"line":341},[332,3685,3686],{},"    if (exception != null) {\n",[332,3688,3689],{"class":334,"line":347},[332,3690,3691],{},"        \u002F\u002F 记录原始消息、告警并进入补偿流程\n",[332,3693,3694],{"class":334,"line":2235},[332,3695,3696],{},"        handleSendFailure(record, exception);\n",[332,3698,3699],{"class":334,"line":2241},[332,3700,3701],{},"    }\n",[332,3703,3704],{"class":334,"line":2279},[332,3705,3706],{},"});\n",[21,3708,3709,3710,3713],{},"序列化失败、鉴权失败、消息过大和超时等错误，最终都需要业务明确处理。不能把“调用过 ",[25,3711,3712],{},"send","”当成“消息已经可靠进入 Kafka”。",[89,3715,3717],{"id":3716},"_4-数据库与-kafka-的一致性","4. 数据库与 Kafka 的一致性",[21,3719,3720],{},"如果业务流程是：",[97,3722,3725],{"className":3723,"code":3724,"language":102,"meta":103},[100],"更新数据库\n→\n发送 Kafka 消息\n",[25,3726,3724],{"__ignoreMap":103},[21,3728,3729],{},"数据库更新成功、消息发送失败时，仍然会造成业务事件丢失。",[21,3731,3732],{},"常见解决方式是本地消息表，也叫 Transactional Outbox：",[97,3734,3737],{"className":3735,"code":3736,"language":102,"meta":103},[100],"同一个数据库事务\n├── 更新业务数据\n└── 写入待发送事件表\n\n事务提交后\n→ 后台任务投递 Kafka\n→ 成功后标记事件已发送\n",[25,3738,3736],{"__ignoreMap":103},[21,3740,3741],{},"这样即使 Kafka 暂时不可用，消息也可以从本地消息表继续重试。Kafka 事务可以保证 Kafka 内部多条记录及消费 Offset 的原子性，但不会自动把外部数据库事务包含进来。",[14,3743,3745],{"id":3744},"三broker-如何保证消息可靠保存","三、Broker 如何保证消息可靠保存",[89,3747,3749],{"id":3748},"_1-合理设置副本数","1. 合理设置副本数",[21,3751,3752],{},"生产环境通常使用：",[97,3754,3757],{"className":3755,"code":3756,"language":102,"meta":103},[100],"replication.factor = 3\n",[25,3758,3756],{"__ignoreMap":103},[21,3760,3761],{},"一个分区包含一个 Leader 和多个 Follower。生产者与 Leader 交互，Follower 从 Leader 复制日志。某个 Broker 故障后，可以从仍然同步的副本中选举新 Leader。",[21,3763,3764],{},"副本数提高的是容错能力，但副本只有真正保持同步才有意义。",[89,3766,3768],{"id":3767},"_2-理解-isr","2. 理解 ISR",[21,3770,3771],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。",[21,3773,3774],{},"例如一个三副本分区：",[97,3776,3779],{"className":3777,"code":3778,"language":102,"meta":103},[100],"Leader A\nFollower B\nFollower C\n\nISR = [A, B, C]\n",[25,3780,3778],{"__ignoreMap":103},[21,3782,3783],{},"如果 C 长时间跟不上 Leader，它会被移出 ISR：",[97,3785,3788],{"className":3786,"code":3787,"language":102,"meta":103},[100],"ISR = [A, B]\n",[25,3789,3787],{"__ignoreMap":103},[21,3791,3792,3794],{},[25,3793,31],{}," 等待的是当前 ISR 的确认，而不是永远等待所有配置副本。",[89,3796,3798,3799],{"id":3797},"_3-配置-mininsyncreplicas","3. 配置 ",[25,3800,35],{},[21,3802,3803],{},"推荐组合：",[97,3805,3807],{"className":3806,"code":576,"language":102,"meta":103},[100],[25,3808,576],{"__ignoreMap":103},[21,3810,3811],{},"它表达的含义是：",[18,3813,3814],{},[21,3815,3816],{},"至少要有两个同步副本可用，写入才允许成功。",[21,3818,3819],{},"如果 ISR 只剩一个副本，Broker 会拒绝写入。此时系统牺牲部分可用性，避免在单副本状态下继续写入并承担更高的数据丢失风险。",[89,3821,3823],{"id":3822},"_4-关闭非同步副本选举","4. 关闭非同步副本选举",[97,3825,3827],{"className":326,"code":3826,"language":328,"meta":103,"style":103},"unclean.leader.election.enable=false\n",[25,3828,3829],{"__ignoreMap":103},[332,3830,3831],{"class":334,"line":335},[332,3832,3826],{},[21,3834,3835,3838],{},[25,3836,3837],{},"unclean leader election"," 指允许不在 ISR 中的副本被选举为 Leader。如果所有同步副本都不可用，非 ISR 副本可能缺少最新消息；将它选为 Leader 虽然能更快恢复分区服务，却可能造成日志回退和数据丢失。",[21,3840,3841],{},"因此，对数据可靠性要求较高的场景应明确配置：",[18,3843,3844],{},[21,3845,3846],{},[45,3847,3848,3849,911],{},"关闭非同步副本选举：",[25,3850,3443],{},[21,3852,3853],{},"这是一个典型的取舍：",[97,3855,3858],{"className":3856,"code":3857,"language":102,"meta":103},[100],"开启非同步副本选举：可用性更高，但可能丢失消息\n关闭非同步副本选举：数据可靠性更强，但分区可能暂时不可用\n",[25,3859,3857],{"__ignoreMap":103},[89,3861,3863,3864,3866,3867],{"id":3862},"_5-acksall-不等于每条消息都立即-fsync","5. ",[25,3865,31],{}," 不等于每条消息都立即 ",[25,3868,260],{},[21,3870,3871,3872,911],{},"Kafka 的持久性主要依赖顺序写日志、操作系统页缓存和多副本机制。Broker 返回成功，并不表示每个副本都对该消息单独执行了一次物理磁盘 ",[25,3873,260],{},[21,3875,3876],{},"实际生产中，通常通过跨 Broker 副本降低单机和单盘故障风险，而不是强制每条消息同步刷盘。若多个副本同时发生不可恢复故障，仍不存在数学意义上的绝对零丢失。",[14,3878,3880],{"id":3879},"四消费者如何避免消费丢失","四、消费者如何避免“消费丢失”",[89,3882,3884],{"id":3883},"_1-关闭自动提交-offset","1. 关闭自动提交 Offset",[97,3886,3888],{"className":326,"code":3887,"language":328,"meta":103,"style":103},"enable.auto.commit=false\n",[25,3889,3890],{"__ignoreMap":103},[332,3891,3892],{"class":334,"line":335},[332,3893,3887],{},[21,3895,3896],{},"自动提交可能出现以下顺序：",[97,3898,3901],{"className":3899,"code":3900,"language":102,"meta":103},[100],"拉取消息\n→ 自动提交 Offset\n→ 执行业务\n→ 进程崩溃\n",[25,3902,3900],{"__ignoreMap":103},[21,3904,3905],{},"重启后，消费者会从已提交的下一个 Offset 继续消费，刚才尚未处理成功的消息就被跳过了。",[89,3907,3909],{"id":3908},"_2-业务成功后再提交-offset","2. 业务成功后再提交 Offset",[21,3911,3912],{},"更可靠的顺序是：",[97,3914,3917],{"className":3915,"code":3916,"language":102,"meta":103},[100],"拉取消息\n→ 执行业务\n→ 业务成功\n→ 提交 Offset\n",[25,3918,3916],{"__ignoreMap":103},[21,3920,3921],{},"如果业务处理失败，就不提交 Offset，让消息后续重新消费：",[97,3923,3925],{"className":1966,"code":3924,"language":1968,"meta":103,"style":103},"while (true) {\n    ConsumerRecords\u003CString, String> records = consumer.poll(Duration.ofSeconds(1));\n\n    for (ConsumerRecord\u003CString, String> record : records) {\n        process(record);\n    }\n\n    consumer.commitSync();\n}\n",[25,3926,3927,3932,3937,3941,3946,3951,3955,3959,3964],{"__ignoreMap":103},[332,3928,3929],{"class":334,"line":335},[332,3930,3931],{},"while (true) {\n",[332,3933,3934],{"class":334,"line":341},[332,3935,3936],{},"    ConsumerRecords\u003CString, String> records = consumer.poll(Duration.ofSeconds(1));\n",[332,3938,3939],{"class":334,"line":347},[332,3940,2262],{"emptyLinePlaceholder":1073},[332,3942,3943],{"class":334,"line":2235},[332,3944,3945],{},"    for (ConsumerRecord\u003CString, String> record : records) {\n",[332,3947,3948],{"class":334,"line":2241},[332,3949,3950],{},"        process(record);\n",[332,3952,3953],{"class":334,"line":2279},[332,3954,3701],{},[332,3956,3957],{"class":334,"line":2285},[332,3958,2262],{"emptyLinePlaceholder":1073},[332,3960,3961],{"class":334,"line":2291},[332,3962,3963],{},"    consumer.commitSync();\n",[332,3965,3966],{"class":334,"line":2296},[332,3967,2288],{},[21,3969,3970],{},"示例表达的是基本原则。实际批量消费时，如果一批消息中只有部分成功，需要准确管理各分区可提交的 Offset，不能直接把失败消息之后的位置一起提交。",[89,3972,3974],{"id":3973},"_3-为什么还需要业务幂等","3. 为什么还需要业务幂等",[21,3976,3977],{},"“处理成功后提交”可以避免消息被跳过，但会引入重复：",[97,3979,3982],{"className":3980,"code":3981,"language":102,"meta":103},[100],"业务处理成功\n→ 提交 Offset 前进程崩溃\n→ 重启后再次消费同一条消息\n",[25,3983,3981],{"__ignoreMap":103},[21,3985,3986],{},"因此，可靠消费通常采用至少一次投递，并在业务层保证幂等。常见方式包括：",[55,3988,3989,3992,3995,3998,4001],{},[58,3990,3991],{},"使用事件 ID 建立数据库唯一索引。",[58,3993,3994],{},"建立消费去重表或 Inbox 表。",[58,3996,3997],{},"使用业务状态机，只允许合法状态转换。",[58,3999,4000],{},"更新时带版本号或业务条件。",[58,4002,4003],{},"对扣款、发券等操作使用唯一业务流水号。",[18,4005,4006],{},[21,4007,4008],{},[45,4009,4010],{},"不丢消息通常意味着允许重复，再通过幂等消除重复影响。",[89,4012,4014],{"id":4013},"_4-kafka-内部链路的-exactly-once","4. Kafka 内部链路的 Exactly Once",[21,4016,4017],{},"如果消费 Kafka 后仍然写回 Kafka，可以使用 Kafka 事务，把输出记录和消费 Offset 放进同一个事务：",[97,4019,4022],{"className":4020,"code":4021,"language":102,"meta":103},[100],"消费消息\n→ 处理\n→ 写入下游 Topic\n→ sendOffsetsToTransaction\n→ 提交事务\n",[25,4023,4021],{"__ignoreMap":103},[21,4025,4026],{},"下游消费者设置：",[97,4028,4030],{"className":326,"code":4029,"language":328,"meta":103,"style":103},"isolation.level=read_committed\n",[25,4031,4032],{"__ignoreMap":103},[332,4033,4034],{"class":334,"line":335},[332,4035,4029],{},[21,4037,4038],{},"这样只读取已提交事务的数据。",[21,4040,4041],{},"但如果最终写入的是 MySQL 等外部系统，Kafka 事务并不能自动保证 Kafka 与数据库的原子性。生产方可以用 Outbox 保证事件可靠发出，消费方可以用 Inbox 应对重复消费，具体区别见下一节。",[14,4043,4045],{"id":4044},"五outbox-与-inbox-分别是什么","五、Outbox 与 Inbox 分别是什么",[21,4047,4048],{},"Outbox 和 Inbox 都借助数据库本地事务解决消息链路中的一致性问题，但它们位于不同位置：",[97,4050,4053],{"className":4051,"code":4052,"language":102,"meta":103},[100],"业务生产方 → Outbox → Kafka → Inbox → 业务消费方\n",[25,4054,4052],{"__ignoreMap":103},[89,4056,4058],{"id":4057},"_1-outbox保证业务事件可靠发出","1. Outbox：保证业务事件可靠发出",[21,4060,4061],{},"Outbox 位于消息生产方，用来解决“数据库更新成功，但 Kafka 消息发送失败”的双写不一致问题。",[21,4063,4064],{},"核心流程：",[97,4066,4069],{"className":4067,"code":4068,"language":102,"meta":103},[100],"同一个数据库事务\n├── 更新业务数据\n└── 写入 Outbox 事件表\n\n事务提交\n→ 后台投递程序扫描 Outbox\n→ 发送 Kafka\n→ 收到成功确认后标记已发送\n",[25,4070,4068],{"__ignoreMap":103},[21,4072,4073],{},"例如创建订单时，不直接把“订单创建事件”的可靠性寄托在一次 Kafka 调用上，而是在创建订单的同一个数据库事务中写入一条 Outbox 记录。即使 Kafka 暂时不可用，后台任务仍可继续重试。",[21,4075,4076],{},"Outbox 的关键点：",[55,4078,4079,4082,4088,4091,4094],{},[58,4080,4081],{},"业务数据和事件记录必须写入同一个数据库事务。",[58,4083,4084,4085,911],{},"每条事件应有全局唯一的 ",[25,4086,4087],{},"eventId",[58,4089,4090],{},"投递程序可以轮询事件表，也可以通过 CDC 捕获新增事件。",[58,4092,4093],{},"“发送成功但标记失败”可能导致再次投递，因此消费者仍需幂等。",[58,4095,4096],{},"Outbox 解决的是事件可靠发出，不保证消费者一定处理成功。",[21,4098,4099,4100,4103],{},"这里的 CDC 是 ",[45,4101,4102],{},"Change Data Capture（变更数据捕获）","。它会读取数据库的变更日志，例如 MySQL Binlog，捕获数据的新增、修改和删除，再把变更发送到 Kafka。常用工具有 Debezium、Canal。",[21,4105,4106],{},"在 Outbox 场景中，它的工作链路是：",[97,4108,4111],{"className":4109,"code":4110,"language":102,"meta":103},[100],"业务事务写入业务表和 Outbox 表\n→ CDC 监听 Binlog\n→ 捕获新增的 Outbox 记录\n→ 发送到 Kafka\n",[25,4112,4110],{"__ignoreMap":103},[21,4114,4115,4116,4118],{},"相比定时扫描 Outbox 表，CDC 延迟更低，也能减少频繁查询数据库的压力。但消息链路仍可能产生重复，因此必须保留 ",[25,4117,4087],{},"，并由消费端保证幂等。",[89,4120,4122],{"id":4121},"_2-inbox保证重复消息只产生一次业务效果","2. Inbox：保证重复消息只产生一次业务效果",[21,4124,4125],{},"Inbox 位于消息消费方，用来记录已经处理过的事件，主要解决至少一次投递带来的重复消费问题。",[21,4127,4064],{},[97,4129,4132],{"className":4130,"code":4131,"language":102,"meta":103},[100],"收到 Kafka 消息\n→ 开启数据库事务\n→ 根据 eventId 写入 Inbox 记录\n→ 执行业务更新\n→ 提交数据库事务\n→ 提交 Kafka Offset\n",[25,4133,4131],{"__ignoreMap":103},[21,4135,4136,4137,4139],{},"Inbox 表通常对 ",[25,4138,4087],{}," 建立唯一索引。重复消息到达时，如果发现该事件已经存在，就不再重复执行业务逻辑。",[21,4141,4142],{},"Inbox 的关键点：",[55,4144,4145,4148,4151,4154],{},[58,4146,4147],{},"Inbox 记录和业务更新必须处于同一个数据库事务。",[58,4149,4150],{},"唯一索引负责防止同一事件被并发重复处理。",[58,4152,4153],{},"数据库事务提交后、Offset 提交前发生故障，消息仍会重投，但 Inbox 可以识别重复。",[58,4155,4156],{},"如果消费逻辑还会调用外部 HTTP 接口，Inbox 无法自动让数据库和外部接口形成原子事务；仍需下游幂等、重试或补偿。",[89,4158,4160],{"id":4159},"_3-两者的区别","3. 两者的区别",[661,4162,4163,4175],{},[664,4164,4165],{},[667,4166,4167,4169,4172],{},[670,4168,2612],{},[670,4170,4171],{},"Outbox",[670,4173,4174],{},"Inbox",[683,4176,4177,4188,4199,4210,4221],{},[667,4178,4179,4182,4185],{},[688,4180,4181],{},"所在位置",[688,4183,4184],{},"消息生产方",[688,4186,4187],{},"消息消费方",[667,4189,4190,4193,4196],{},[688,4191,4192],{},"主要目标",[688,4194,4195],{},"防止业务成功但事件未发出",[688,4197,4198],{},"防止重复消息重复产生业务效果",[667,4200,4201,4204,4207],{},[688,4202,4203],{},"本地事务内容",[688,4205,4206],{},"业务更新 + 写事件表",[688,4208,4209],{},"写消费记录 + 业务更新",[667,4211,4212,4215,4218],{},[688,4213,4214],{},"后台动作",[688,4216,4217],{},"把待发送事件投递到 Kafka",[688,4219,4220],{},"通常不需要再次投递",[667,4222,4223,4226,4229],{},[688,4224,4225],{},"仍需注意",[688,4227,4228],{},"投递过程可能产生重复消息",[688,4230,4231],{},"外部调用仍需幂等或补偿",[21,4233,4234],{},"一句话区分：",[18,4236,4237],{},[21,4238,4239],{},[45,4240,4241],{},"Outbox 保证“该发的消息最终发出去”，Inbox 保证“重复收到的消息不会重复生效”。",[14,4243,4245],{"id":4244},"六三种投递语义","六、三种投递语义",[661,4247,4248,4261],{},[664,4249,4250],{},[667,4251,4252,4255,4258],{},[670,4253,4254],{},"语义",[670,4256,4257],{},"Offset 与业务处理顺序",[670,4259,4260],{},"结果",[683,4262,4263,4274,4285],{},[667,4264,4265,4268,4271],{},[688,4266,4267],{},"At Most Once",[688,4269,4270],{},"先提交，再处理",[688,4272,4273],{},"可能丢失，通常不重复",[667,4275,4276,4279,4282],{},[688,4277,4278],{},"At Least Once",[688,4280,4281],{},"先处理，再提交",[688,4283,4284],{},"不轻易丢失，可能重复",[667,4286,4287,4290,4293],{},[688,4288,4289],{},"Exactly Once",[688,4291,4292],{},"事务或幂等机制协调",[688,4294,4295],{},"业务效果恰好一次",[21,4297,4298],{},"绝大多数业务系统采用：",[18,4300,4301],{},[21,4302,4303],{},[45,4304,4305],{},"Kafka 至少一次投递 + 消费端业务幂等。",[21,4307,4308],{},"这通常比追求所有环节的强事务更容易实现，也更便于扩展。",[14,4310,4312],{"id":4311},"七常见故障与保障措施","七、常见故障与保障措施",[661,4314,4315,4328],{},[664,4316,4317],{},[667,4318,4319,4322,4325],{},[670,4320,4321],{},"故障场景",[670,4323,4324],{},"可能后果",[670,4326,4327],{},"主要保障",[683,4329,4330,4341,4354,4367,4379,4390,4401,4412],{},[667,4331,4332,4335,4338],{},[688,4333,4334],{},"生产者网络抖动",[688,4336,4337],{},"发送失败或结果未知",[688,4339,4340],{},"重试、幂等生产、回调处理",[667,4342,4343,4346,4349],{},[688,4344,4345],{},"Leader 写入后立即故障",[688,4347,4348],{},"未同步消息丢失",[688,4350,4351,4353],{},[25,4352,31],{},"、副本、ISR",[667,4355,4356,4359,4362],{},[688,4357,4358],{},"ISR 只剩一个副本",[688,4360,4361],{},"单点故障风险升高",[688,4363,4364,4366],{},[25,4365,35],{}," 拒绝写入",[667,4368,4369,4372,4375],{},[688,4370,4371],{},"同步副本全部不可用",[688,4373,4374],{},"非同步副本选主后日志回退",[688,4376,3848,4377],{},[25,4378,3443],{},[667,4380,4381,4384,4387],{},[688,4382,4383],{},"消费者先提交后处理",[688,4385,4386],{},"业务消息被跳过",[688,4388,4389],{},"关闭自动提交，成功后提交",[667,4391,4392,4395,4398],{},[688,4393,4394],{},"处理成功但提交前崩溃",[688,4396,4397],{},"重复消费",[688,4399,4400],{},"业务幂等、唯一流水号",[667,4402,4403,4406,4409],{},[688,4404,4405],{},"数据库成功、Kafka 失败",[688,4407,4408],{},"业务事件未发出",[688,4410,4411],{},"Transactional Outbox",[667,4413,4414,4417,4420],{},[688,4415,4416],{},"Kafka 成功、下游失败",[688,4418,4419],{},"业务结果缺失",[688,4421,4422],{},"重试、死信、补偿、告警",[14,4424,4426],{"id":4425},"八常见误区","八、常见误区",[89,4428,4430,4431,4433],{"id":4429},"误区一配置-acksall-就绝对不会丢","误区一：配置 ",[25,4432,31],{}," 就绝对不会丢",[21,4435,4436,4438,4439,4441],{},[25,4437,31],{}," 只解决生产者到 Broker 的确认强度，还要配合副本数、",[25,4440,35],{},"、正确选主和消费者提交策略。",[89,4443,4445],{"id":4444},"误区二配置无限重试就不会丢","误区二：配置无限重试就不会丢",[21,4447,4448,4449,4451],{},"重试仍然受 ",[25,4450,3644],{}," 等条件限制，而且权限错误、序列化错误等问题无法靠盲目重试解决。最终失败必须被业务感知和补偿。",[89,4453,4455],{"id":4454},"误区三手动提交-offset-就是-exactly-once","误区三：手动提交 Offset 就是 Exactly Once",[21,4457,4458],{},"手动提交只能帮助建立“处理成功后提交”的顺序，崩溃窗口仍可能造成重复消费，因此还需要业务幂等。",[89,4460,4462],{"id":4461},"误区四kafka-事务可以自动覆盖数据库","误区四：Kafka 事务可以自动覆盖数据库",[21,4464,4465],{},"Kafka 事务主要协调 Kafka 内部的消息与 Offset，不会自动与 MySQL 等外部数据库形成同一个原子事务。",[89,4467,4469],{"id":4468},"误区五broker-返回成功就等于所有副本已经物理刷盘","误区五：Broker 返回成功就等于所有副本已经物理刷盘",[21,4471,4472],{},"Kafka 的确认、副本复制和磁盘刷盘是不同概念。高可靠主要依靠多副本和同步副本约束，而不是把每条消息都单独同步刷盘。",[14,4474,4476],{"id":4475},"九监控和运维同样重要","九、监控和运维同样重要",[21,4478,4479],{},"配置正确后，还应持续监控：",[55,4481,4482,4485,4487,4490,4493,4496],{},[58,4483,4484],{},"发送失败率、重试次数和发送延迟。",[58,4486,944],{},[58,4488,4489],{},"Broker 磁盘空间、磁盘延迟和网络异常。",[58,4491,4492],{},"消费积压、消费失败、重平衡次数。",[58,4494,4495],{},"死信消息和补偿任务堆积。",[58,4497,4498],{},"业务事件与最终结果的对账差异。",[21,4500,4501],{},"没有监控和对账，即使消息已经丢失，系统也可能长期无法发现。",[14,4503,967],{"id":967},[55,4505,4506,4511,4516,4521,4527],{},[58,4507,4508],{},[73,4509,998],{"href":996,"rel":4510},[976],[58,4512,4513],{},[73,4514,1012],{"href":1010,"rel":4515},[976],[58,4517,4518],{},[73,4519,1005],{"href":1003,"rel":4520},[976],[58,4522,4523],{},[73,4524,4526],{"href":974,"rel":4525},[976],"Apache Kafka Design：Message Delivery Semantics",[58,4528,4529],{},[73,4530,1019],{"href":1017,"rel":4531},[976],[1021,4533,1023],{},{"title":103,"searchDepth":341,"depth":341,"links":4535},[4536,4537,4538,4539,4546,4555,4561,4566,4567,4568,4576,4577],{"id":16,"depth":341,"text":16},{"id":50,"depth":341,"text":50},{"id":3480,"depth":341,"text":3481},{"id":3504,"depth":341,"text":3505,"children":4540},[4541,4543,4544,4545],{"id":3508,"depth":347,"text":4542},"1. 使用 acks=all",{"id":3595,"depth":347,"text":3596},{"id":3651,"depth":347,"text":3652},{"id":3716,"depth":347,"text":3717},{"id":3744,"depth":341,"text":3745,"children":4547},[4548,4549,4550,4552,4553],{"id":3748,"depth":347,"text":3749},{"id":3767,"depth":347,"text":3768},{"id":3797,"depth":347,"text":4551},"3. 配置 min.insync.replicas",{"id":3822,"depth":347,"text":3823},{"id":3862,"depth":347,"text":4554},"5. acks=all 不等于每条消息都立即 fsync",{"id":3879,"depth":341,"text":3880,"children":4556},[4557,4558,4559,4560],{"id":3883,"depth":347,"text":3884},{"id":3908,"depth":347,"text":3909},{"id":3973,"depth":347,"text":3974},{"id":4013,"depth":347,"text":4014},{"id":4044,"depth":341,"text":4045,"children":4562},[4563,4564,4565],{"id":4057,"depth":347,"text":4058},{"id":4121,"depth":347,"text":4122},{"id":4159,"depth":347,"text":4160},{"id":4244,"depth":341,"text":4245},{"id":4311,"depth":341,"text":4312},{"id":4425,"depth":341,"text":4426,"children":4569},[4570,4572,4573,4574,4575],{"id":4429,"depth":347,"text":4571},"误区一：配置 acks=all 就绝对不会丢",{"id":4444,"depth":347,"text":4445},{"id":4454,"depth":347,"text":4455},{"id":4461,"depth":347,"text":4462},{"id":4468,"depth":347,"text":4469},{"id":4475,"depth":341,"text":4476},{"id":967,"depth":341,"text":967},"wiki:java:kafka-message-loss-prevention","从生产者、Broker 和消费者三段链路分析 Kafka 消息丢失的原因，以及 acks、ISR、副本、手动提交 Offset 和业务幂等的完整保障方案。",{},72,"\u002Fjava\u002Fkafka-message-loss-prevention",{"title":3421,"description":4579},"java\u002Fkafka-message-loss-prevention","mKpAptREGG2pnoZj7JmhkcDtGJ_YBax3G4LfJlMVoxY",{"id":4,"title":5,"body":4587,"commentId":1067,"description":1068,"difficulty":1069,"draft":1070,"extension":1071,"meta":5308,"navigation":1073,"order":1074,"path":1075,"section":1076,"seo":5309,"stem":1078,"updated":1079,"__hash__":1080},{"type":7,"value":4588,"toc":5267},[4589,4591,4593,4603,4605,4611,4613,4615,4625,4632,4634,4636,4638,4643,4645,4649,4651,4653,4655,4663,4665,4667,4669,4671,4676,4678,4680,4682,4684,4686,4688,4690,4695,4697,4699,4701,4706,4720,4722,4724,4726,4731,4733,4743,4747,4749,4751,4756,4758,4770,4774,4779,4781,4783,4789,4791,4807,4813,4815,4817,4819,4829,4839,4841,4843,4848,4852,4857,4859,4866,4870,4872,4878,4882,4884,4886,4888,4893,4895,4900,4902,4907,4909,4911,4913,4925,4927,4929,4931,4933,4938,4940,4942,4944,4946,4951,4953,4959,4961,4966,4968,4970,4972,4974,4980,4982,4992,4994,4996,5001,5003,5005,5009,5014,5016,5018,5054,5056,5061,5063,5065,5067,5069,5074,5076,5078,5124,5126,5131,5133,5135,5149,5151,5153,5158,5160,5162,5164,5166,5168,5172,5174,5176,5178,5180,5184,5190,5192,5194,5202,5204,5214,5216,5226,5228,5265],[10,4590,5],{"id":12},[14,4592,16],{"id":16},[18,4594,4595],{},[21,4596,23,4597,28,4599,32,4601,36],{},[25,4598,27],{},[25,4600,31],{},[25,4602,35],{},[21,4604,39],{},[18,4606,4607],{},[21,4608,4609],{},[45,4610,47],{},[14,4612,50],{"id":50},[21,4614,53],{},[55,4616,4617,4619,4621,4623],{},[58,4618,60],{},[58,4620,63],{},[58,4622,66],{},[58,4624,69],{},[21,4626,4627],{},[73,4628,4630],{"href":75,"target":76,"rel":4629},[78,79],[81,4631],{"src":75,"alt":83},[14,4633,87],{"id":86},[89,4635,92],{"id":91},[21,4637,95],{},[97,4639,4641],{"className":4640,"code":101,"language":102,"meta":103},[100],[25,4642,101],{"__ignoreMap":103},[21,4644,108],{},[21,4646,111,4647,115],{},[45,4648,114],{},[89,4650,119],{"id":118},[21,4652,122],{},[21,4654,125],{},[55,4656,4657,4659,4661],{},[58,4658,130],{},[58,4660,133],{},[58,4662,136],{},[21,4664,139],{},[89,4666,143],{"id":142},[21,4668,146],{},[21,4670,149],{},[97,4672,4674],{"className":4673,"code":153,"language":102,"meta":103},[100],[25,4675,153],{"__ignoreMap":103},[21,4677,158],{},[89,4679,162],{"id":161},[21,4681,165],{},[21,4683,168],{},[14,4685,172],{"id":171},[89,4687,176],{"id":175},[21,4689,179],{},[97,4691,4693],{"className":4692,"code":183,"language":102,"meta":103},[100],[25,4694,183],{"__ignoreMap":103},[21,4696,188],{},[89,4698,192],{"id":191},[21,4700,195],{},[97,4702,4704],{"className":4703,"code":199,"language":102,"meta":103},[100],[25,4705,199],{"__ignoreMap":103},[55,4707,4708,4712,4716],{},[58,4709,4710,209],{},[25,4711,208],{},[58,4713,4714,215],{},[25,4715,214],{},[58,4717,4718,221],{},[25,4719,220],{},[21,4721,224],{},[89,4723,228],{"id":227},[21,4725,231],{},[97,4727,4729],{"className":4728,"code":235,"language":102,"meta":103},[100],[25,4730,235],{"__ignoreMap":103},[21,4732,240],{},[55,4734,4735,4737,4739,4741],{},[58,4736,245],{},[58,4738,248],{},[58,4740,251],{},[58,4742,254],{},[21,4744,257,4745,261],{},[25,4746,260],{},[89,4748,265],{"id":264},[21,4750,268],{},[97,4752,4754],{"className":4753,"code":272,"language":102,"meta":103},[100],[25,4755,272],{"__ignoreMap":103},[21,4757,277],{},[55,4759,4760,4762,4764,4766,4768],{},[58,4761,282],{},[58,4763,285],{},[58,4765,288],{},[58,4767,291],{},[58,4769,294],{},[21,4771,297,4772,301],{},[45,4773,300],{},[97,4775,4777],{"className":4776,"code":305,"language":102,"meta":103},[100],[25,4778,305],{"__ignoreMap":103},[21,4780,310],{},[21,4782,313],{},[18,4784,4785],{},[21,4786,4787],{},[45,4788,320],{},[21,4790,323],{},[97,4792,4793],{"className":326,"code":327,"language":328,"meta":103,"style":103},[25,4794,4795,4799,4803],{"__ignoreMap":103},[332,4796,4797],{"class":334,"line":335},[332,4798,338],{},[332,4800,4801],{"class":334,"line":341},[332,4802,344],{},[332,4804,4805],{"class":334,"line":347},[332,4806,350],{},[21,4808,4809,356,4811,360],{},[25,4810,355],{},[25,4812,359],{},[89,4814,364],{"id":363},[21,4816,367],{},[21,4818,370],{},[55,4820,4821,4823,4825,4827],{},[58,4822,375],{},[58,4824,378],{},[58,4826,381],{},[58,4828,384],{},[21,4830,387,4831,32,4833,32,4835,397,4837,401],{},[25,4832,390],{},[25,4834,393],{},[25,4836,396],{},[25,4838,400],{},[89,4840,405],{"id":404},[21,4842,408],{},[97,4844,4846],{"className":4845,"code":412,"language":102,"meta":103},[100],[25,4847,412],{"__ignoreMap":103},[21,4849,417,4850,420],{},[25,4851,27],{},[97,4853,4855],{"className":4854,"code":424,"language":102,"meta":103},[100],[25,4856,424],{"__ignoreMap":103},[21,4858,429],{},[21,4860,4861],{},[73,4862,4864],{"href":434,"target":76,"rel":4863},[78,79],[81,4865],{"src":434,"alt":438},[21,4867,441,4868,444],{},[25,4869,27],{},[21,4871,447],{},[18,4873,4874],{},[21,4875,4876],{},[45,4877,454],{},[21,4879,457,4880,460],{},[25,4881,27],{},[89,4883,464],{"id":463},[21,4885,467],{},[21,4887,470],{},[97,4889,4891],{"className":4890,"code":474,"language":102,"meta":103},[100],[25,4892,474],{"__ignoreMap":103},[21,4894,149],{},[97,4896,4898],{"className":4897,"code":482,"language":102,"meta":103},[100],[25,4899,482],{"__ignoreMap":103},[21,4901,487],{},[97,4903,4905],{"className":4904,"code":491,"language":102,"meta":103},[100],[25,4906,491],{"__ignoreMap":103},[21,4908,496],{},[21,4910,499],{},[21,4912,502],{},[97,4914,4915],{"className":326,"code":505,"language":328,"meta":103,"style":103},[25,4916,4917,4921],{"__ignoreMap":103},[332,4918,4919],{"class":334,"line":335},[332,4920,512],{},[332,4922,4923],{"class":334,"line":341},[332,4924,517],{},[21,4926,520],{},[14,4928,524],{"id":523},[89,4930,528],{"id":527},[21,4932,531],{},[97,4934,4936],{"className":4935,"code":535,"language":102,"meta":103},[100],[25,4937,535],{"__ignoreMap":103},[21,4939,540],{},[21,4941,543],{},[89,4943,547],{"id":546},[21,4945,550],{},[97,4947,4949],{"className":4948,"code":554,"language":102,"meta":103},[100],[25,4950,554],{"__ignoreMap":103},[21,4952,559],{},[89,4954,563,4955,567,4957],{"id":562},[25,4956,566],{},[25,4958,35],{},[21,4960,572],{},[97,4962,4964],{"className":4963,"code":576,"language":102,"meta":103},[100],[25,4965,576],{"__ignoreMap":103},[21,4967,581],{},[21,4969,584],{},[89,4971,588],{"id":587},[590,4973,593],{"id":592},[21,4975,596,4976,600,4978,604],{},[25,4977,599],{},[45,4979,603],{},[21,4981,607],{},[55,4983,4984,4986,4988,4990],{},[58,4985,612],{},[58,4987,615],{},[58,4989,618],{},[58,4991,621],{},[21,4993,624],{},[21,4995,627],{},[97,4997,4999],{"className":4998,"code":631,"language":102,"meta":103},[100],[25,5000,631],{"__ignoreMap":103},[21,5002,636],{},[590,5004,640],{"id":639},[21,5006,643,5007,647],{},[45,5008,646],{},[97,5010,5012],{"className":5011,"code":651,"language":102,"meta":103},[100],[25,5013,651],{"__ignoreMap":103},[21,5015,656],{},[21,5017,659],{},[661,5019,5020,5032],{},[664,5021,5022],{},[667,5023,5024,5026,5028,5030],{},[670,5025,672],{},[670,5027,675],{},[670,5029,678],{},[670,5031,681],{},[683,5033,5034,5044],{},[667,5035,5036,5038,5040,5042],{},[688,5037,690],{},[688,5039,693],{},[688,5041,696],{},[688,5043,699],{},[667,5045,5046,5048,5050,5052],{},[688,5047,704],{},[688,5049,707],{},[688,5051,710],{},[688,5053,713],{},[21,5055,716],{},[97,5057,5059],{"className":5058,"code":720,"language":102,"meta":103},[100],[25,5060,720],{"__ignoreMap":103},[21,5062,725],{},[89,5064,729],{"id":728},[21,5066,732],{},[21,5068,735],{},[97,5070,5072],{"className":5071,"code":739,"language":102,"meta":103},[100],[25,5073,739],{"__ignoreMap":103},[21,5075,744],{},[14,5077,748],{"id":747},[661,5079,5080,5092],{},[664,5081,5082],{},[667,5083,5084,5086,5088,5090],{},[670,5085,757],{},[670,5087,760],{},[670,5089,763],{},[670,5091,766],{},[683,5093,5094,5104,5114],{},[667,5095,5096,5098,5100,5102],{},[688,5097,773],{},[688,5099,776],{},[688,5101,779],{},[688,5103,782],{},[667,5105,5106,5108,5110,5112],{},[688,5107,787],{},[688,5109,790],{},[688,5111,793],{},[688,5113,796],{},[667,5115,5116,5118,5120,5122],{},[688,5117,801],{},[688,5119,804],{},[688,5121,807],{},[688,5123,810],{},[21,5125,813],{},[97,5127,5129],{"className":5128,"code":817,"language":102,"meta":103},[100],[25,5130,817],{"__ignoreMap":103},[14,5132,823],{"id":822},[21,5134,826],{},[55,5136,5137,5139,5141,5143,5145,5147],{},[58,5138,831],{},[58,5140,834],{},[58,5142,837],{},[58,5144,840],{},[58,5146,843],{},[58,5148,846],{},[21,5150,849],{},[21,5152,852],{},[97,5154,5156],{"className":5155,"code":856,"language":102,"meta":103},[100],[25,5157,856],{"__ignoreMap":103},[21,5159,861],{},[14,5161,865],{"id":864},[89,5163,869],{"id":868},[21,5165,872],{},[89,5167,876],{"id":875},[21,5169,879,5170,882],{},[25,5171,27],{},[89,5173,886],{"id":885},[21,5175,889],{},[89,5177,893],{"id":892},[21,5179,896],{},[89,5181,900,5182,903],{"id":899},[25,5183,31],{},[21,5185,5186,908,5188,911],{},[25,5187,31],{},[25,5189,35],{},[14,5191,915],{"id":914},[89,5193,918],{"id":918},[55,5195,5196,5198,5200],{},[58,5197,923],{},[58,5199,926],{},[58,5201,929],{},[89,5203,933],{"id":932},[55,5205,5206,5208,5210,5212],{},[58,5207,938],{},[58,5209,941],{},[58,5211,944],{},[58,5213,947],{},[89,5215,950],{"id":950},[55,5217,5218,5220,5222,5224],{},[58,5219,955],{},[58,5221,958],{},[58,5223,961],{},[58,5225,964],{},[14,5227,967],{"id":967},[55,5229,5230,5235,5240,5245,5250,5255,5260],{},[58,5231,5232],{},[73,5233,977],{"href":974,"rel":5234},[976],[58,5236,5237],{},[73,5238,984],{"href":982,"rel":5239},[976],[58,5241,5242],{},[73,5243,991],{"href":989,"rel":5244},[976],[58,5246,5247],{},[73,5248,998],{"href":996,"rel":5249},[976],[58,5251,5252],{},[73,5253,1005],{"href":1003,"rel":5254},[976],[58,5256,5257],{},[73,5258,1012],{"href":1010,"rel":5259},[976],[58,5261,5262],{},[73,5263,1019],{"href":1017,"rel":5264},[976],[1021,5266,1023],{},{"title":103,"searchDepth":341,"depth":341,"links":5268},[5269,5270,5271,5277,5286,5293,5294,5295,5302,5307],{"id":16,"depth":341,"text":16},{"id":50,"depth":341,"text":50},{"id":86,"depth":341,"text":87,"children":5272},[5273,5274,5275,5276],{"id":91,"depth":347,"text":92},{"id":118,"depth":347,"text":119},{"id":142,"depth":347,"text":143},{"id":161,"depth":347,"text":162},{"id":171,"depth":341,"text":172,"children":5278},[5279,5280,5281,5282,5283,5284,5285],{"id":175,"depth":347,"text":176},{"id":191,"depth":347,"text":192},{"id":227,"depth":347,"text":228},{"id":264,"depth":347,"text":265},{"id":363,"depth":347,"text":364},{"id":404,"depth":347,"text":405},{"id":463,"depth":347,"text":464},{"id":523,"depth":341,"text":524,"children":5287},[5288,5289,5290,5291,5292],{"id":527,"depth":347,"text":528},{"id":546,"depth":347,"text":547},{"id":562,"depth":347,"text":1048},{"id":587,"depth":347,"text":588},{"id":728,"depth":347,"text":729},{"id":747,"depth":341,"text":748},{"id":822,"depth":341,"text":823},{"id":864,"depth":341,"text":865,"children":5296},[5297,5298,5299,5300,5301],{"id":868,"depth":347,"text":869},{"id":875,"depth":347,"text":876},{"id":885,"depth":347,"text":886},{"id":892,"depth":347,"text":893},{"id":899,"depth":347,"text":1060},{"id":914,"depth":341,"text":915,"children":5303},[5304,5305,5306],{"id":918,"depth":347,"text":918},{"id":932,"depth":347,"text":933},{"id":950,"depth":347,"text":950},{"id":967,"depth":341,"text":967},{},{"title":5,"description":1068},1784862035925]