【Java EE】TOCTOU

什么是 TOCTOU

TOCTOU = T ime O f C heck T o T ime O f U se,即 检查时间到使用时间 的竞态条件。

核心定义:在并发环境下,程序先检查某个条件(如文件是否存在、变量是否为 null、库存是否足够),然后在稍后的时间点基于该检查结果执行操作。如果在这两个时间点之间,共享状态被另一个线程/进程修改,那么"检查"的结果就已经过时了,"使用"就会基于错误的前提做出错误的行为。

CWE 编号\[CWE-367] --- Time-of-check Time-of-use (TOCTOU) Race Condition

成立条件(三个要素缺一不可)

要素 说明
共享可变状态 存在多个执行流都能访问并修改的数据
非原子化操作 检查(Check)和使用(Use)是两个独立的步骤
时间窗口 Check 和 Use 之间存在"间隙",允许并发修改发生

本质:违反原子性

从数据库的 ACID 角度看,TOCTOU 本质上就是缺少了原子性(Atomicity)和隔离性(Isolation)保护的并发操作。

TOCTOU 的经典攻击时序

共享资源 共享资源 #mermaid-svg-8OZHv6zO8jEHgP1C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8OZHv6zO8jEHgP1C .error-icon{fill:#552222;}#mermaid-svg-8OZHv6zO8jEHgP1C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8OZHv6zO8jEHgP1C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8OZHv6zO8jEHgP1C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8OZHv6zO8jEHgP1C .marker.cross{stroke:#333333;}#mermaid-svg-8OZHv6zO8jEHgP1C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8OZHv6zO8jEHgP1C p{margin:0;}#mermaid-svg-8OZHv6zO8jEHgP1C .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OZHv6zO8jEHgP1C text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OZHv6zO8jEHgP1C .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8OZHv6zO8jEHgP1C .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8OZHv6zO8jEHgP1C #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8OZHv6zO8jEHgP1C .sequenceNumber{fill:white;}#mermaid-svg-8OZHv6zO8jEHgP1C #sequencenumber{fill:#333;}#mermaid-svg-8OZHv6zO8jEHgP1C #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8OZHv6zO8jEHgP1C .messageText{fill:#333;stroke:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OZHv6zO8jEHgP1C .labelText,#mermaid-svg-8OZHv6zO8jEHgP1C .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .loopText,#mermaid-svg-8OZHv6zO8jEHgP1C .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OZHv6zO8jEHgP1C .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8OZHv6zO8jEHgP1C .noteText,#mermaid-svg-8OZHv6zO8jEHgP1C .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8OZHv6zO8jEHgP1C .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OZHv6zO8jEHgP1C .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OZHv6zO8jEHgP1C .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OZHv6zO8jEHgP1C .actorPopupMenu{position:absolute;}#mermaid-svg-8OZHv6zO8jEHgP1C .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8OZHv6zO8jEHgP1C .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OZHv6zO8jEHgP1C .actor-man circle,#mermaid-svg-8OZHv6zO8jEHgP1C line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8OZHv6zO8jEHgP1C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠️ 竞态窗口开始 攻击者在此窗口内 改变了共享状态 ⚠️ 竞态窗口结束 结果:写入错误文件 / 数据不一致 / 安全绕过 攻击者线程B 受害者线程A 🔍 1. Check: 条件成立 ✓ 🔧 2. 修改共享状态 💣 3. Use: 基于过时条件执行操作 攻击者线程B 受害者线程A

关键洞察 :攻击者无需"快过"受害者,只需要 恰好命中那个窗口。在高并发系统中,这个窗口可能非常大(例如两次系统调用之间)。

Java 中的 TOCTOU 场景分类

在 Java 中 TOCTOU 表现形式非常多。核心思想完全一致:在多线程或并发环境下,在检查和使用之间存在一个时间窗口,共享状态在这个窗口里被其他线程修改了,导致你基于过时的检查结果做出了错误行为。
#mermaid-svg-Hwsc8M51jEFrTAkj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hwsc8M51jEFrTAkj .error-icon{fill:#552222;}#mermaid-svg-Hwsc8M51jEFrTAkj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hwsc8M51jEFrTAkj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hwsc8M51jEFrTAkj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hwsc8M51jEFrTAkj .marker.cross{stroke:#333333;}#mermaid-svg-Hwsc8M51jEFrTAkj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hwsc8M51jEFrTAkj p{margin:0;}#mermaid-svg-Hwsc8M51jEFrTAkj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster-label text{fill:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster-label span{color:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster-label span p{background-color:transparent;}#mermaid-svg-Hwsc8M51jEFrTAkj .label text,#mermaid-svg-Hwsc8M51jEFrTAkj span{fill:#333;color:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj .node rect,#mermaid-svg-Hwsc8M51jEFrTAkj .node circle,#mermaid-svg-Hwsc8M51jEFrTAkj .node ellipse,#mermaid-svg-Hwsc8M51jEFrTAkj .node polygon,#mermaid-svg-Hwsc8M51jEFrTAkj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Hwsc8M51jEFrTAkj .rough-node .label text,#mermaid-svg-Hwsc8M51jEFrTAkj .node .label text,#mermaid-svg-Hwsc8M51jEFrTAkj .image-shape .label,#mermaid-svg-Hwsc8M51jEFrTAkj .icon-shape .label{text-anchor:middle;}#mermaid-svg-Hwsc8M51jEFrTAkj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Hwsc8M51jEFrTAkj .rough-node .label,#mermaid-svg-Hwsc8M51jEFrTAkj .node .label,#mermaid-svg-Hwsc8M51jEFrTAkj .image-shape .label,#mermaid-svg-Hwsc8M51jEFrTAkj .icon-shape .label{text-align:center;}#mermaid-svg-Hwsc8M51jEFrTAkj .node.clickable{cursor:pointer;}#mermaid-svg-Hwsc8M51jEFrTAkj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Hwsc8M51jEFrTAkj .arrowheadPath{fill:#333333;}#mermaid-svg-Hwsc8M51jEFrTAkj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Hwsc8M51jEFrTAkj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Hwsc8M51jEFrTAkj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hwsc8M51jEFrTAkj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Hwsc8M51jEFrTAkj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hwsc8M51jEFrTAkj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster text{fill:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj .cluster span{color:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Hwsc8M51jEFrTAkj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Hwsc8M51jEFrTAkj rect.text{fill:none;stroke-width:0;}#mermaid-svg-Hwsc8M51jEFrTAkj .icon-shape,#mermaid-svg-Hwsc8M51jEFrTAkj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hwsc8M51jEFrTAkj .icon-shape p,#mermaid-svg-Hwsc8M51jEFrTAkj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Hwsc8M51jEFrTAkj .icon-shape .label rect,#mermaid-svg-Hwsc8M51jEFrTAkj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hwsc8M51jEFrTAkj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Hwsc8M51jEFrTAkj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Hwsc8M51jEFrTAkj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Java TOCTOU 竞态条件
文件系统操作
多线程 Check-Then-Act
数据库事务
懒初始化
分布式系统
access + open 模式
临时文件创建
路径遍历/符号链接
非线程安全集合
volatile 缺失
读写不配对
SELECT + UPDATE
Read-Modify-Write
双重检查锁定
指令重排问题

文件系统操作

和 C 语言中的 access() + open() 一模一样,Java 中如果先检查文件属性,再操作文件,就会面临 TOCTOU 风险。

文件操作 TOCTOU 原理

文件系统 文件系统 #mermaid-svg-n2099KtoI3PB8Tbs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n2099KtoI3PB8Tbs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n2099KtoI3PB8Tbs .error-icon{fill:#552222;}#mermaid-svg-n2099KtoI3PB8Tbs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n2099KtoI3PB8Tbs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n2099KtoI3PB8Tbs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n2099KtoI3PB8Tbs .marker.cross{stroke:#333333;}#mermaid-svg-n2099KtoI3PB8Tbs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n2099KtoI3PB8Tbs p{margin:0;}#mermaid-svg-n2099KtoI3PB8Tbs .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n2099KtoI3PB8Tbs text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-n2099KtoI3PB8Tbs .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-n2099KtoI3PB8Tbs .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-n2099KtoI3PB8Tbs .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-n2099KtoI3PB8Tbs .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-n2099KtoI3PB8Tbs #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-n2099KtoI3PB8Tbs .sequenceNumber{fill:white;}#mermaid-svg-n2099KtoI3PB8Tbs #sequencenumber{fill:#333;}#mermaid-svg-n2099KtoI3PB8Tbs #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-n2099KtoI3PB8Tbs .messageText{fill:#333;stroke:none;}#mermaid-svg-n2099KtoI3PB8Tbs .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n2099KtoI3PB8Tbs .labelText,#mermaid-svg-n2099KtoI3PB8Tbs .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-n2099KtoI3PB8Tbs .loopText,#mermaid-svg-n2099KtoI3PB8Tbs .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-n2099KtoI3PB8Tbs .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-n2099KtoI3PB8Tbs .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-n2099KtoI3PB8Tbs .noteText,#mermaid-svg-n2099KtoI3PB8Tbs .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-n2099KtoI3PB8Tbs .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n2099KtoI3PB8Tbs .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n2099KtoI3PB8Tbs .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n2099KtoI3PB8Tbs .actorPopupMenu{position:absolute;}#mermaid-svg-n2099KtoI3PB8Tbs .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-n2099KtoI3PB8Tbs .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n2099KtoI3PB8Tbs .actor-man circle,#mermaid-svg-n2099KtoI3PB8Tbs line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-n2099KtoI3PB8Tbs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠️ 竞态窗口 竞态窗口关闭 如果是 root 权限运行 → 特权文件被覆盖! Java 应用程序(高权限) 普通用户/攻击者 🔍 检查: f.exists() && f.canWrite() ✓ true(指向 /tmp/userfile) rm /tmp/userfile ln -s /etc/shadow /tmp/userfile 💣 new FileOutputStream(f) 实际写入 /etc/shadow Java 应用程序(高权限) 普通用户/攻击者

错误示例

java 复制代码
// ❌ 反模式:先检查后打开
File f = new File("/tmp/userfile");
// 检查时间:检查文件是否存在、是否可写
if (f.exists() && f.canWrite()) {
    // 使用时间:中间可能被攻击者替换成符号链接
    // 如果你的程序以高权限运行,就可能写入特权文件
    try (FileOutputStream out = new FileOutputStream(f)) {
        out.write(data);
    }
}

攻击者可以在你检查后、打开文件前,把 /tmp/userfile 替换成指向 /etc/shadow 的符号链接。

正确做法(Java NIO)

方案 A:直接操作 + 异常处理

java 复制代码
// ✅ 原则:不要"先问再动",直接"动手,失败了再说"
Path path = Paths.get("/tmp/userfile");
try (OutputStream out = Files.newOutputStream(path, 
         StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    // 文件已成功打开,通过已打开的文件句柄验证属性
    // 此时对文件句柄的属性检查是安全的------
    // 因为文件已经被我们"持有"了
    out.write(data);
} catch (AccessDeniedException e) {
    // 权限不足
} catch (NoSuchFileException e) {
    // 文件不存在
}

方案 B:禁止跟随符号链接

java 复制代码
// ✅ 更安全:打开文件时明确禁止跟随符号链接
Files.newOutputStream(path, 
    StandardOpenOption.CREATE, 
    StandardOpenOption.WRITE, 
    LinkOption.NOFOLLOW_LINKS);  // 关键:拒绝符号链接

方案 C:使用 Files 的原子属性操作

java 复制代码
// ✅ 读取属性时也禁止跟随符号链接
BasicFileAttributes attrs = Files.readAttributes(
    path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);

// 然后再用同样的选项打开文件
if (!attrs.isDirectory()) {
    Files.newOutputStream(path, StandardOpenOption.WRITE, 
        LinkOption.NOFOLLOW_LINKS);
}

⚠️ 注意:即使 readAttributesnewOutputStream 都用了 NOFOLLOW_LINKS,它们之间仍然存在理论上的 TOCTOU 窗口。最安全的做法始终是直接打开文件并基于返回的 Channel/Stream 做后续判断。

多线程下的 Check-Then-Act 模式

这是 Java 中最经典、最普遍 的 TOCTOU 竞态。任何 if (condition) then action 的代码块,如果 condition 涉及的共享变量没有被正确保护,就会出问题。

竞态窗口示意图

ArrayList(共享) ArrayList(共享) #mermaid-svg-zkEf72m9cO4UF6NW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zkEf72m9cO4UF6NW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zkEf72m9cO4UF6NW .error-icon{fill:#552222;}#mermaid-svg-zkEf72m9cO4UF6NW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zkEf72m9cO4UF6NW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zkEf72m9cO4UF6NW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zkEf72m9cO4UF6NW .marker.cross{stroke:#333333;}#mermaid-svg-zkEf72m9cO4UF6NW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zkEf72m9cO4UF6NW p{margin:0;}#mermaid-svg-zkEf72m9cO4UF6NW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zkEf72m9cO4UF6NW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zkEf72m9cO4UF6NW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zkEf72m9cO4UF6NW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zkEf72m9cO4UF6NW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zkEf72m9cO4UF6NW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zkEf72m9cO4UF6NW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zkEf72m9cO4UF6NW .sequenceNumber{fill:white;}#mermaid-svg-zkEf72m9cO4UF6NW #sequencenumber{fill:#333;}#mermaid-svg-zkEf72m9cO4UF6NW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zkEf72m9cO4UF6NW .messageText{fill:#333;stroke:none;}#mermaid-svg-zkEf72m9cO4UF6NW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zkEf72m9cO4UF6NW .labelText,#mermaid-svg-zkEf72m9cO4UF6NW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zkEf72m9cO4UF6NW .loopText,#mermaid-svg-zkEf72m9cO4UF6NW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zkEf72m9cO4UF6NW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zkEf72m9cO4UF6NW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zkEf72m9cO4UF6NW .noteText,#mermaid-svg-zkEf72m9cO4UF6NW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zkEf72m9cO4UF6NW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zkEf72m9cO4UF6NW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zkEf72m9cO4UF6NW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zkEf72m9cO4UF6NW .actorPopupMenu{position:absolute;}#mermaid-svg-zkEf72m9cO4UF6NW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zkEf72m9cO4UF6NW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zkEf72m9cO4UF6NW .actor-man circle,#mermaid-svg-zkEf72m9cO4UF6NW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zkEf72m9cO4UF6NW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠️ T1 还不知道 key 已被删除! 线程1 线程2 🔍 contains("key") → true 🔧 remove("key") 💣 remove("key") → 抛出异常或逻辑错误 线程1 线程2

典型例子 1:非线程安全的集合

java 复制代码
// ❌ 共享 ArrayList(非线程安全)
// 存在 TOCTOU 竞态
List<String> list = new ArrayList<>();  // 被多个线程共享

// 线程 A:
if (list.contains("key")) {          // T1: 检查 → true
    // ⚠️ 另一个线程刚好在此刻删除了 "key"
    list.remove("key");              // T2: 使用 → 可能为 false,remove 返回 false
}

// 线程 B(并发执行):
list.remove("key");                   // 在 T1 和 T2 之间执行

结果分析 :线程 A 的 remove("key") 静默失败(返回 false),但代码逻辑以为是成功删除了一个存在的元素。这种静默的逻辑错误在线上非常难以排查。

方案 A:并发集合的原子操作

java 复制代码
// ✅ 使用 ConcurrentHashMap 的原子复合操作
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 原子地"如果存在则删除"
String removed = map.remove("key"); 
// 不需要先 containsKey 再 remove --- remove 本身就是原子的

// ✅ 其他并发集合的原子方法
ConcurrentMap<String, String> cmap = new ConcurrentHashMap<>();
cmap.putIfAbsent("key", "value");     // 不存在才放入
cmap.replace("key", "old", "new");    // CAS 替换
cmap.compute("key", (k, v) -> ...);   // 原子计算并更新

方案 B:synchronized 临界区

java 复制代码
// ✅ 把检查和操作包裹成一个原子单元
private final Object lock = new Object();

synchronized (lock) {
    if (list.contains("key")) {
        list.remove("key");
    }
}

设计原则 :synchronized 块要刚好覆盖检查和操作,不能太小(漏出竞态窗口),也不能太大(影响吞吐量)。

方案 C:读写锁(ReadWriteLock)

java 复制代码
// ✅ 适合读多写少场景
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, String> map = new HashMap<>();

public void safeRemove(String key) {
    rwLock.writeLock().lock();
    try {
        if (map.containsKey(key)) {
            map.remove(key);
        }
    } finally {
        rwLock.writeLock().unlock();
    }
}

懒初始化单例:Double-Checked Locking 的 TOCTOU

这是 Java 并发编程史上最著名的 TOCTOU 漏洞之一,也是面试高频考点。

问题代码

java 复制代码
// ❌ 经典的双重检查锁定 ------ 有 TOCTOU 漏洞!
public class Singleton {
    private static Singleton instance;  // 没有 volatile!

    public static Singleton getInstance() {
        if (instance == null) {                    // ① 第一次检查(非同步)
            synchronized (Singleton.class) {
                if (instance == null) {            // ② 第二次检查(同步块内)
                    instance = new Singleton();    // ③ 问题发生在这里!
                }
            }
        }
        return instance;                           // ④ 使用
    }
}

指令重排

instance = new Singleton() 在 JVM 层面不是原子操作,它分解为:
#mermaid-svg-Ao5GgqCRnZRHOO3g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ao5GgqCRnZRHOO3g .error-icon{fill:#552222;}#mermaid-svg-Ao5GgqCRnZRHOO3g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ao5GgqCRnZRHOO3g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .marker.cross{stroke:#333333;}#mermaid-svg-Ao5GgqCRnZRHOO3g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ao5GgqCRnZRHOO3g p{margin:0;}#mermaid-svg-Ao5GgqCRnZRHOO3g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster-label text{fill:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster-label span{color:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster-label span p{background-color:transparent;}#mermaid-svg-Ao5GgqCRnZRHOO3g .label text,#mermaid-svg-Ao5GgqCRnZRHOO3g span{fill:#333;color:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .node rect,#mermaid-svg-Ao5GgqCRnZRHOO3g .node circle,#mermaid-svg-Ao5GgqCRnZRHOO3g .node ellipse,#mermaid-svg-Ao5GgqCRnZRHOO3g .node polygon,#mermaid-svg-Ao5GgqCRnZRHOO3g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .rough-node .label text,#mermaid-svg-Ao5GgqCRnZRHOO3g .node .label text,#mermaid-svg-Ao5GgqCRnZRHOO3g .image-shape .label,#mermaid-svg-Ao5GgqCRnZRHOO3g .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ao5GgqCRnZRHOO3g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .rough-node .label,#mermaid-svg-Ao5GgqCRnZRHOO3g .node .label,#mermaid-svg-Ao5GgqCRnZRHOO3g .image-shape .label,#mermaid-svg-Ao5GgqCRnZRHOO3g .icon-shape .label{text-align:center;}#mermaid-svg-Ao5GgqCRnZRHOO3g .node.clickable{cursor:pointer;}#mermaid-svg-Ao5GgqCRnZRHOO3g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .arrowheadPath{fill:#333333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ao5GgqCRnZRHOO3g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ao5GgqCRnZRHOO3g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ao5GgqCRnZRHOO3g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster text{fill:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g .cluster span{color:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ao5GgqCRnZRHOO3g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ao5GgqCRnZRHOO3g rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ao5GgqCRnZRHOO3g .icon-shape,#mermaid-svg-Ao5GgqCRnZRHOO3g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ao5GgqCRnZRHOO3g .icon-shape p,#mermaid-svg-Ao5GgqCRnZRHOO3g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ao5GgqCRnZRHOO3g .icon-shape .label rect,#mermaid-svg-Ao5GgqCRnZRHOO3g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ao5GgqCRnZRHOO3g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ao5GgqCRnZRHOO3g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ao5GgqCRnZRHOO3g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} JIT/CPU 可能重排为

  1. 分配堆内存空间
  2. 将引用 instance 指向内存地址

⚠️ 此时对象尚未初始化!
3. 调用构造函数初始化对象
new Singleton() 的实际执行步骤

  1. 分配堆内存空间
  2. 调用构造函数初始化对象
  3. 将引用 instance 指向内存地址
    主内存 主内存 #mermaid-svg-I2CNF3no3otQqAmT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I2CNF3no3otQqAmT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I2CNF3no3otQqAmT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I2CNF3no3otQqAmT .error-icon{fill:#552222;}#mermaid-svg-I2CNF3no3otQqAmT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I2CNF3no3otQqAmT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I2CNF3no3otQqAmT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I2CNF3no3otQqAmT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I2CNF3no3otQqAmT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I2CNF3no3otQqAmT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I2CNF3no3otQqAmT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I2CNF3no3otQqAmT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I2CNF3no3otQqAmT .marker.cross{stroke:#333333;}#mermaid-svg-I2CNF3no3otQqAmT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I2CNF3no3otQqAmT p{margin:0;}#mermaid-svg-I2CNF3no3otQqAmT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2CNF3no3otQqAmT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-I2CNF3no3otQqAmT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I2CNF3no3otQqAmT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-I2CNF3no3otQqAmT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-I2CNF3no3otQqAmT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-I2CNF3no3otQqAmT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-I2CNF3no3otQqAmT .sequenceNumber{fill:white;}#mermaid-svg-I2CNF3no3otQqAmT #sequencenumber{fill:#333;}#mermaid-svg-I2CNF3no3otQqAmT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-I2CNF3no3otQqAmT .messageText{fill:#333;stroke:none;}#mermaid-svg-I2CNF3no3otQqAmT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2CNF3no3otQqAmT .labelText,#mermaid-svg-I2CNF3no3otQqAmT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-I2CNF3no3otQqAmT .loopText,#mermaid-svg-I2CNF3no3otQqAmT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-I2CNF3no3otQqAmT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I2CNF3no3otQqAmT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-I2CNF3no3otQqAmT .noteText,#mermaid-svg-I2CNF3no3otQqAmT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-I2CNF3no3otQqAmT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2CNF3no3otQqAmT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2CNF3no3otQqAmT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2CNF3no3otQqAmT .actorPopupMenu{position:absolute;}#mermaid-svg-I2CNF3no3otQqAmT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-I2CNF3no3otQqAmT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2CNF3no3otQqAmT .actor-man circle,#mermaid-svg-I2CNF3no3otQqAmT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-I2CNF3no3otQqAmT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 指令重排:先赋值引用 再初始化对象 看到非 null,但对象未完全初始化 可能读到字段的默认值(0/false/null) 而非构造器中设置的值! 线程1 线程2 分配内存,instance 指向新地址 🔍 检查 instance == null → false! 💣 使用 instance(部分构造的对象) 构造函数完成初始化 线程1 线程2

Java 5+ 标准修复:

java 复制代码
// ✅ 加上 volatile 禁止指令重排
public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();  // volatile 保证:写完才可见
                }
            }
        }
        return instance;
    }
}

volatile 的语义保证:

  1. 可见性:一个线程写入 volatile 变量后,其他线程立即可见
  2. 禁止指令重排:volatile 写之前的操作不会被重排到写之后(即"构造函数执行完"一定发生在 "instance 赋值" 被其他线程看到之前)

更优方案:利用类加载机制

java 复制代码
// ✅ 利用 JVM 类加载本身就是线程安全的
public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;  // 首次访问 Holder 时才触发类加载
    }
}

这个方案利用 JVM 的类加载锁保证了初始化只发生一次,且没有使用任何显式锁,既安全又高效。

原子变量与 CAS 操作

Java 并发包里的 AtomicIntegerAtomicReference 等提供的方法(如 compareAndSet)本质上是把"检查"和"更新"合并成一个不可分割的硬件原子操作,直接从根本上消除了 TOCTOU 窗口。

CAS 原理

线程 内存 CPU(硬件原子指令) 线程 内存 CPU(硬件原子指令) #mermaid-svg-0SqSTTX0CxiNkBTu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0SqSTTX0CxiNkBTu .error-icon{fill:#552222;}#mermaid-svg-0SqSTTX0CxiNkBTu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0SqSTTX0CxiNkBTu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0SqSTTX0CxiNkBTu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0SqSTTX0CxiNkBTu .marker.cross{stroke:#333333;}#mermaid-svg-0SqSTTX0CxiNkBTu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0SqSTTX0CxiNkBTu p{margin:0;}#mermaid-svg-0SqSTTX0CxiNkBTu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0SqSTTX0CxiNkBTu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0SqSTTX0CxiNkBTu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0SqSTTX0CxiNkBTu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0SqSTTX0CxiNkBTu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0SqSTTX0CxiNkBTu .sequenceNumber{fill:white;}#mermaid-svg-0SqSTTX0CxiNkBTu #sequencenumber{fill:#333;}#mermaid-svg-0SqSTTX0CxiNkBTu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0SqSTTX0CxiNkBTu .messageText{fill:#333;stroke:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0SqSTTX0CxiNkBTu .labelText,#mermaid-svg-0SqSTTX0CxiNkBTu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .loopText,#mermaid-svg-0SqSTTX0CxiNkBTu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0SqSTTX0CxiNkBTu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0SqSTTX0CxiNkBTu .noteText,#mermaid-svg-0SqSTTX0CxiNkBTu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0SqSTTX0CxiNkBTu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0SqSTTX0CxiNkBTu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0SqSTTX0CxiNkBTu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0SqSTTX0CxiNkBTu .actorPopupMenu{position:absolute;}#mermaid-svg-0SqSTTX0CxiNkBTu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0SqSTTX0CxiNkBTu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0SqSTTX0CxiNkBTu .actor-man circle,#mermaid-svg-0SqSTTX0CxiNkBTu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0SqSTTX0CxiNkBTu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CAS: Compare-And-Swap alt 当前值 == 期望值 当前值 != 期望值 发起 CAS(期望值, 新值) 读取当前值 原子写入新值 ✓ 成功 ✗ 失败(说明中间被改过)

关键特性 :CAS 指令在 CPU 指令层面 就是原子的------没有"检查后、更新前"的窗口,因为"比较"和"写入"是同一条硬件指令完成的。

java 复制代码
// ❌ 非原子操作(有 TOCTOU)
int current = counter.get();
//  ⚠️ 竞态窗口:其他线程可能在此修改 counter
if (current < max) {
    counter.set(current + 1);   // 可能覆盖其他线程的更新
}

// ✅ 原子操作(CAS 循环,无 TOCTOU)
AtomicInteger counter = new AtomicInteger(0);
int current;
do {
    current = counter.get();           // 读取当前值
    if (current >= max) {
        break;                          // 达到上限,退出
    }
    // CAS: "如果当前值还是 current,则更新为 current+1"
    // 如果不是(说明被改了),则重试
} while (!counter.compareAndSet(current, current + 1));

CAS 的活锁问题与解决方案

CAS 循环重试可能导致活锁(livelock)------线程一直在重试但总被其他线程抢先。

java 复制代码
// ✅ 使用 LongAdder(JDK 8+)避免高竞争下的 CAS 自旋开销
LongAdder adder = new LongAdder();
adder.increment();  // 内部做了分段累积,降低 CAS 竞争

// ✅ 使用 AtomicIntegerFieldUpdater 减少对象创建开销
private static final AtomicIntegerFieldUpdater<MyClass> UPDATER =
    AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "counter");
private volatile int counter;

设计启示 :CAS 是在乐观并发假设下工作的(假设冲突不频繁)。如果冲突频繁发生,CAS 自旋的开销可能超过直接加锁。选择 CAS 还是锁,取决于实际的竞争强度。

数据库事务层面的 TOCTOU

Java 应用中经常使用数据库,如果先 SELECTUPDATE,而没有使用锁或乐观版本号,就是典型的 TOCTOU 竞态。

库存扣减场景

数据库(商品库存=1) 数据库(商品库存=1) #mermaid-svg-lL12APnacscxWQVJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lL12APnacscxWQVJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lL12APnacscxWQVJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lL12APnacscxWQVJ .error-icon{fill:#552222;}#mermaid-svg-lL12APnacscxWQVJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lL12APnacscxWQVJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lL12APnacscxWQVJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lL12APnacscxWQVJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lL12APnacscxWQVJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lL12APnacscxWQVJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lL12APnacscxWQVJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lL12APnacscxWQVJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lL12APnacscxWQVJ .marker.cross{stroke:#333333;}#mermaid-svg-lL12APnacscxWQVJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lL12APnacscxWQVJ p{margin:0;}#mermaid-svg-lL12APnacscxWQVJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lL12APnacscxWQVJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lL12APnacscxWQVJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lL12APnacscxWQVJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lL12APnacscxWQVJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lL12APnacscxWQVJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lL12APnacscxWQVJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lL12APnacscxWQVJ .sequenceNumber{fill:white;}#mermaid-svg-lL12APnacscxWQVJ #sequencenumber{fill:#333;}#mermaid-svg-lL12APnacscxWQVJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lL12APnacscxWQVJ .messageText{fill:#333;stroke:none;}#mermaid-svg-lL12APnacscxWQVJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lL12APnacscxWQVJ .labelText,#mermaid-svg-lL12APnacscxWQVJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lL12APnacscxWQVJ .loopText,#mermaid-svg-lL12APnacscxWQVJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lL12APnacscxWQVJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lL12APnacscxWQVJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lL12APnacscxWQVJ .noteText,#mermaid-svg-lL12APnacscxWQVJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lL12APnacscxWQVJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lL12APnacscxWQVJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lL12APnacscxWQVJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lL12APnacscxWQVJ .actorPopupMenu{position:absolute;}#mermaid-svg-lL12APnacscxWQVJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lL12APnacscxWQVJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lL12APnacscxWQVJ .actor-man circle,#mermaid-svg-lL12APnacscxWQVJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lL12APnacscxWQVJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⚠️ 两个线程都认为库存充足 库存变为 0 💣 库存变为 -1!超卖了! 线程A(订单1) 线程B(订单2) SELECT stock FROM product WHERE id=1 stock = 1(够用!) SELECT stock FROM product WHERE id=1 stock = 1(也够用!) UPDATE product SET stock = stock - 1 UPDATE product SET stock = stock - 1 线程A(订单1) 线程B(订单2)

java 复制代码
// ❌ SELECT 后 UPDATE ------ 存在 TOCTOU 竞态
@Transactional
public void deductStock(int productId, int quantity) {
    // 隔离级别 READ_COMMITTED 下,两次 SELECT 可能读到不同值
    int stock = jdbcTemplate.queryForObject(
        "SELECT stock FROM product WHERE id = ?", 
        Integer.class, productId);
    
    if (stock >= quantity) {
        // ⚠️ 另一个事务可能在这个间隙也查到 stock >= quantity
        jdbcTemplate.update(
            "UPDATE product SET stock = stock - ? WHERE id = ?", 
            quantity, productId);
    }
}

#mermaid-svg-Zxqmjbr4MBmGnJUU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Zxqmjbr4MBmGnJUU .error-icon{fill:#552222;}#mermaid-svg-Zxqmjbr4MBmGnJUU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Zxqmjbr4MBmGnJUU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .marker.cross{stroke:#333333;}#mermaid-svg-Zxqmjbr4MBmGnJUU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Zxqmjbr4MBmGnJUU p{margin:0;}#mermaid-svg-Zxqmjbr4MBmGnJUU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster-label text{fill:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster-label span{color:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster-label span p{background-color:transparent;}#mermaid-svg-Zxqmjbr4MBmGnJUU .label text,#mermaid-svg-Zxqmjbr4MBmGnJUU span{fill:#333;color:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .node rect,#mermaid-svg-Zxqmjbr4MBmGnJUU .node circle,#mermaid-svg-Zxqmjbr4MBmGnJUU .node ellipse,#mermaid-svg-Zxqmjbr4MBmGnJUU .node polygon,#mermaid-svg-Zxqmjbr4MBmGnJUU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .rough-node .label text,#mermaid-svg-Zxqmjbr4MBmGnJUU .node .label text,#mermaid-svg-Zxqmjbr4MBmGnJUU .image-shape .label,#mermaid-svg-Zxqmjbr4MBmGnJUU .icon-shape .label{text-anchor:middle;}#mermaid-svg-Zxqmjbr4MBmGnJUU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .rough-node .label,#mermaid-svg-Zxqmjbr4MBmGnJUU .node .label,#mermaid-svg-Zxqmjbr4MBmGnJUU .image-shape .label,#mermaid-svg-Zxqmjbr4MBmGnJUU .icon-shape .label{text-align:center;}#mermaid-svg-Zxqmjbr4MBmGnJUU .node.clickable{cursor:pointer;}#mermaid-svg-Zxqmjbr4MBmGnJUU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .arrowheadPath{fill:#333333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zxqmjbr4MBmGnJUU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Zxqmjbr4MBmGnJUU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zxqmjbr4MBmGnJUU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster text{fill:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU .cluster span{color:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Zxqmjbr4MBmGnJUU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Zxqmjbr4MBmGnJUU rect.text{fill:none;stroke-width:0;}#mermaid-svg-Zxqmjbr4MBmGnJUU .icon-shape,#mermaid-svg-Zxqmjbr4MBmGnJUU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Zxqmjbr4MBmGnJUU .icon-shape p,#mermaid-svg-Zxqmjbr4MBmGnJUU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Zxqmjbr4MBmGnJUU .icon-shape .label rect,#mermaid-svg-Zxqmjbr4MBmGnJUU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Zxqmjbr4MBmGnJUU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Zxqmjbr4MBmGnJUU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Zxqmjbr4MBmGnJUU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SELECT + UPDATE TOCTOU
方案1: 原子SQL
方案2: 悲观锁
方案3: 乐观锁
WHERE stock >= quantity

让数据库在 UPDATE 时重新检查条件
SELECT ... FOR UPDATE

行级锁,阻止并发修改
version 字段

UPDATE ... SET version = version+1

WHERE version = 旧版本号

方案 A:原子 SQL(最简单)

java 复制代码
// ✅ 一条 SQL 完成检查+修改,数据库保证原子性
public boolean deductStock(int productId, int quantity) {
    int affected = jdbcTemplate.update(
        "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?",
        quantity, productId, quantity);
    return affected > 0;  // 影响行数 > 0 表示扣减成功
}

方案 B:悲观锁(SELECT ... FOR UPDATE)

java 复制代码
// ✅ 在 SELECT 时就锁定行,阻止并发修改
@Transactional
public void deductStockWithLock(int productId, int quantity) {
    // FOR UPDATE 对该行加排他锁,其他事务的 FOR UPDATE 会阻塞
    Integer stock = jdbcTemplate.queryForObject(
        "SELECT stock FROM product WHERE id = ? FOR UPDATE",
        Integer.class, productId);
    
    if (stock != null && stock >= quantity) {
        jdbcTemplate.update(
            "UPDATE product SET stock = stock - ? WHERE id = ?",
            quantity, productId);
    }
}

⚠️ 悲观锁代价:锁持有期间其他事务无法修改该行,可能成为性能瓶颈。适合竞争激烈的场景。

方案 C:乐观锁(版本号)

java 复制代码
// ✅ 使用 version 字段实现乐观锁
@Entity
public class Product {
    @Id private Long id;
    private Integer stock;
    
    @Version  // JPA 自动管理版本号
    private Long version;
}

@Transactional
public void deductStockOptimistic(int productId, int quantity) {
    Product product = entityManager.find(Product.class, productId);
    if (product.getStock() >= quantity) {
        product.setStock(product.getStock() - quantity);
        // JPA 会自动在 UPDATE 时检查 version,不匹配则抛 OptimisticLockException
        // UPDATE product SET stock=?, version=version+1 
        // WHERE id=? AND version=?
    }
}

乐观锁适合竞争不激烈的场景,失败后可以重试。它不需要持有行锁,吞吐量更高。

TOCTOU 防御策略总结

#mermaid-svg-hkHnLdrvkVwpGSY2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hkHnLdrvkVwpGSY2 .error-icon{fill:#552222;}#mermaid-svg-hkHnLdrvkVwpGSY2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hkHnLdrvkVwpGSY2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .marker.cross{stroke:#333333;}#mermaid-svg-hkHnLdrvkVwpGSY2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hkHnLdrvkVwpGSY2 p{margin:0;}#mermaid-svg-hkHnLdrvkVwpGSY2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster-label text{fill:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster-label span{color:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster-label span p{background-color:transparent;}#mermaid-svg-hkHnLdrvkVwpGSY2 .label text,#mermaid-svg-hkHnLdrvkVwpGSY2 span{fill:#333;color:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .node rect,#mermaid-svg-hkHnLdrvkVwpGSY2 .node circle,#mermaid-svg-hkHnLdrvkVwpGSY2 .node ellipse,#mermaid-svg-hkHnLdrvkVwpGSY2 .node polygon,#mermaid-svg-hkHnLdrvkVwpGSY2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .rough-node .label text,#mermaid-svg-hkHnLdrvkVwpGSY2 .node .label text,#mermaid-svg-hkHnLdrvkVwpGSY2 .image-shape .label,#mermaid-svg-hkHnLdrvkVwpGSY2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-hkHnLdrvkVwpGSY2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .rough-node .label,#mermaid-svg-hkHnLdrvkVwpGSY2 .node .label,#mermaid-svg-hkHnLdrvkVwpGSY2 .image-shape .label,#mermaid-svg-hkHnLdrvkVwpGSY2 .icon-shape .label{text-align:center;}#mermaid-svg-hkHnLdrvkVwpGSY2 .node.clickable{cursor:pointer;}#mermaid-svg-hkHnLdrvkVwpGSY2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .arrowheadPath{fill:#333333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hkHnLdrvkVwpGSY2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hkHnLdrvkVwpGSY2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hkHnLdrvkVwpGSY2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster text{fill:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 .cluster span{color:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hkHnLdrvkVwpGSY2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hkHnLdrvkVwpGSY2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-hkHnLdrvkVwpGSY2 .icon-shape,#mermaid-svg-hkHnLdrvkVwpGSY2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hkHnLdrvkVwpGSY2 .icon-shape p,#mermaid-svg-hkHnLdrvkVwpGSY2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hkHnLdrvkVwpGSY2 .icon-shape .label rect,#mermaid-svg-hkHnLdrvkVwpGSY2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hkHnLdrvkVwpGSY2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hkHnLdrvkVwpGSY2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hkHnLdrvkVwpGSY2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🛡️ 三条防御路线
原子化

Atomicity
串行化

Serialization
反转控制

Act-First
CAS / 原子变量
并发集合原子方法
原子 SQL 语句
putIfAbsent / remove
synchronized 临界区
ReentrantLock
ReadWriteLock
SELECT ... FOR UPDATE
分布式锁(Redis/ZK)
直接打开文件,失败即异常
乐观锁 + 重试
基于 Channel/句柄验证属性

策略 核心思想 典型手段 适用场景
原子化 把 Check + Use 合并为一个不可分割的操作 CAS、并发集合原子方法、原子 SQL 竞争适中、操作简单
串行化 用锁把 Check-Use 区域保护起来 synchronized、Lock、悲观锁 竞争激烈、操作复杂
反转控制 先做(Act),失败就说明条件不满足 直接打开文件 + 异常处理、乐观锁重试 I/O 操作、竞争不激烈

拓展:分布式系统中的 TOCTOU

TOCTOU 不仅限于单机场景,在分布式系统中同样普遍:

java 复制代码
// ❌ Redis 中的 TOCTOU:先检查锁是否存在再获取
if (!redis.exists("lock:order:123")) {         // Check
    // ⚠️ 另一个节点可能在此间隙获取了锁
    redis.set("lock:order:123", nodeId);        // Use
}

// ✅ 使用 SETNX 原子命令(SET if Not eXists)
boolean acquired = redis.setnx("lock:order:123", nodeId, 
    "EX", 30);  // 设置过期时间 30 秒
// SETNX 本身就是原子的"检查并设置"

类似的例子还包括 ZooKeeper 中先 exists()create()、数据库分库分表场景下的分布式事务等。

知识清单 Checklist

  • 能用自己的话解释 TOCTOU 的三个成立条件
  • 能画出 TOCTOU 攻击的时序图
  • 能写出 Double-Checked Locking 的问题代码和正确修复
  • 理解 volatile 在 DCL 中的作用(可见性 + 禁止重排)
  • 能用 CAS 写一个无锁的 Check-Then-Act 循环
  • 知道 CWE-367 编号
  • 能区分"原子化"、"串行化"、"反转控制"三种防御策略
  • 能用一条原子 SQL 解决库存超卖问题
  • 理解 Files.newOutputStream + LinkOption.NOFOLLOW_LINKS 的用法
  • 知道分布式场景中 Redis SETNX 为何优于 exists + set

参考 资料

  • \[CWE-367] --- Time-of-check Time-of-use (TOCTOU) Race Condition
  • 《Java 并发编程实战》(Java Concurrency in Practice )--- Brian Goetz
    • 第 16 章:Java 内存模型(DCL 问题详解)
    • 第 15 章:原子变量与非阻塞同步(CAS)
  • OWASP: File System Race Condition
  • CVE-2024-38819 --- Spring Framework 中的路径遍历 TOCTOU 漏洞(真实案例)
相关推荐
.千余1 小时前
【C++】 String 常用操作:增删查改 | 查找 | 截取 | IO
java·服务器·开发语言·c++·笔记·学习
长栎1 小时前
面试官说你的单例线程不安全,你真能现场修好?
java
码云骑士1 小时前
【Java基础】JDK安装常见问题教辅-从踩坑到排雷
java·开发语言
Sunia1 小时前
《AgentX 专栏》09-MCP协议双向打通:让AgentX既能被Claude调用又能调度全球工具生态
java·架构
wyu729611 小时前
SpringBoot八股的一些概念笔记
java·面试
一只积极向上的小咸鱼1 小时前
TOML、JSON、YAML、INI 配置文件格式总结
java·服务器·json
莫逸风1 小时前
【AgentScope】4.会话(Session)详解
java·llm·agent·agentscope
吴阿福|一人公司1 小时前
类变量和实例变量的命名规范有哪些具体的例子?
java·开发语言
eddietao1 小时前
什么是 fail-fast?什么是 fail-safe?
java·面试