Android 开发里很容易遇到这种问题:后台线程里有个循环任务,主线程点了"取消",你把
running = false改掉了,可后台线程像没看见一样还在跑。或者你写了一个单例,觉得if (instance == null)判断两次就很稳,结果面试官继续追问:为什么这里一定要加volatile?
一句话结论: volatile 适合解决"一个线程写状态,其他线程读状态"的轻量同步问题;它保证可见性和一定的有序性,但不保证 count++ 这种复合操作的原子性。
本文按 Java 8+ / Android ART 的常见开发场景来讲,底层依据是 Java 内存模型,也就是 JMM。具体到 JVM、ART、CPU 指令和内存屏障会有实现差异,文章里只讲应用开发和面试最需要掌握的那层。
1. 先从一个真的会踩坑的取消任务说起
先看一个很普通的写法:
java
class ImageScanTask implements Runnable {
private boolean running = true;
public void cancel() {
running = false;
}
@Override
public void run() {
while (running) {
scanOneImage();
}
}
}
如果 run() 在线程 A 里执行,cancel() 在线程 B 里执行,这段代码看起来应该能停下来。但问题是:线程 A 不一定每次循环都重新从共享内存里读取 running。
编译器、运行时和 CPU 都可能做优化。对线程 A 来说,如果没有同步约束,它可能认为 running 在自己的执行路径里没有变化,于是一直用缓存里的旧值。
#mermaid-svg-M0jaRD8yrZ9FeO46{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-M0jaRD8yrZ9FeO46 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M0jaRD8yrZ9FeO46 .error-icon{fill:#552222;}#mermaid-svg-M0jaRD8yrZ9FeO46 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M0jaRD8yrZ9FeO46 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .marker.cross{stroke:#333333;}#mermaid-svg-M0jaRD8yrZ9FeO46 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M0jaRD8yrZ9FeO46 p{margin:0;}#mermaid-svg-M0jaRD8yrZ9FeO46 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster-label text{fill:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster-label span{color:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster-label span p{background-color:transparent;}#mermaid-svg-M0jaRD8yrZ9FeO46 .label text,#mermaid-svg-M0jaRD8yrZ9FeO46 span{fill:#333;color:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .node rect,#mermaid-svg-M0jaRD8yrZ9FeO46 .node circle,#mermaid-svg-M0jaRD8yrZ9FeO46 .node ellipse,#mermaid-svg-M0jaRD8yrZ9FeO46 .node polygon,#mermaid-svg-M0jaRD8yrZ9FeO46 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .rough-node .label text,#mermaid-svg-M0jaRD8yrZ9FeO46 .node .label text,#mermaid-svg-M0jaRD8yrZ9FeO46 .image-shape .label,#mermaid-svg-M0jaRD8yrZ9FeO46 .icon-shape .label{text-anchor:middle;}#mermaid-svg-M0jaRD8yrZ9FeO46 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .rough-node .label,#mermaid-svg-M0jaRD8yrZ9FeO46 .node .label,#mermaid-svg-M0jaRD8yrZ9FeO46 .image-shape .label,#mermaid-svg-M0jaRD8yrZ9FeO46 .icon-shape .label{text-align:center;}#mermaid-svg-M0jaRD8yrZ9FeO46 .node.clickable{cursor:pointer;}#mermaid-svg-M0jaRD8yrZ9FeO46 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .arrowheadPath{fill:#333333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0jaRD8yrZ9FeO46 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M0jaRD8yrZ9FeO46 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0jaRD8yrZ9FeO46 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster text{fill:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 .cluster span{color:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 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-M0jaRD8yrZ9FeO46 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M0jaRD8yrZ9FeO46 rect.text{fill:none;stroke-width:0;}#mermaid-svg-M0jaRD8yrZ9FeO46 .icon-shape,#mermaid-svg-M0jaRD8yrZ9FeO46 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M0jaRD8yrZ9FeO46 .icon-shape p,#mermaid-svg-M0jaRD8yrZ9FeO46 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M0jaRD8yrZ9FeO46 .icon-shape .label rect,#mermaid-svg-M0jaRD8yrZ9FeO46 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M0jaRD8yrZ9FeO46 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M0jaRD8yrZ9FeO46 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M0jaRD8yrZ9FeO46 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 A 执行循环任务
读取 running 为 true
继续使用旧值
线程 B 调用 cancel
写入 running 为 false
没有同步关系
线程 A 可能继续循环
这不是说每台机器都会复现,也不是说普通变量一定看不到新值。真正的问题是:Java 内存模型没有给这段代码一个"必须看见"的保证。所以线上偶现一次,就已经够难查了。
把字段改成 volatile:
java
class ImageScanTask implements Runnable {
private volatile boolean running = true;
public void cancel() {
running = false;
}
@Override
public void run() {
while (running) {
scanOneImage();
}
}
}
这时线程 B 对 running 的写入,对线程 A 后续读取 running 是可见的。用更面试一点的话说:对同一个 volatile 字段的写,happens-before 后续对这个字段的读。
2. volatile 到底保证什么
别急着背"可见性、有序性、不保证原子性"。这句话对,但太像口号。我们换成人话。
2.1 可见性:别人改了,我下次读要能看见
普通字段的问题是:线程之间读写同一个变量,如果没有同步关系,读线程可能看到旧值。
volatile 的第一层作用就是给这个字段建立可见性规则。一个线程写入 volatile 字段后,其他线程后续读取这个字段,要能看到这次写入,或者看到更晚的写入。
#mermaid-svg-6Bvbc7MrxzTUVC1f{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-6Bvbc7MrxzTUVC1f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6Bvbc7MrxzTUVC1f .error-icon{fill:#552222;}#mermaid-svg-6Bvbc7MrxzTUVC1f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6Bvbc7MrxzTUVC1f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .marker.cross{stroke:#333333;}#mermaid-svg-6Bvbc7MrxzTUVC1f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6Bvbc7MrxzTUVC1f p{margin:0;}#mermaid-svg-6Bvbc7MrxzTUVC1f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster-label text{fill:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster-label span{color:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster-label span p{background-color:transparent;}#mermaid-svg-6Bvbc7MrxzTUVC1f .label text,#mermaid-svg-6Bvbc7MrxzTUVC1f span{fill:#333;color:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .node rect,#mermaid-svg-6Bvbc7MrxzTUVC1f .node circle,#mermaid-svg-6Bvbc7MrxzTUVC1f .node ellipse,#mermaid-svg-6Bvbc7MrxzTUVC1f .node polygon,#mermaid-svg-6Bvbc7MrxzTUVC1f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .rough-node .label text,#mermaid-svg-6Bvbc7MrxzTUVC1f .node .label text,#mermaid-svg-6Bvbc7MrxzTUVC1f .image-shape .label,#mermaid-svg-6Bvbc7MrxzTUVC1f .icon-shape .label{text-anchor:middle;}#mermaid-svg-6Bvbc7MrxzTUVC1f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .rough-node .label,#mermaid-svg-6Bvbc7MrxzTUVC1f .node .label,#mermaid-svg-6Bvbc7MrxzTUVC1f .image-shape .label,#mermaid-svg-6Bvbc7MrxzTUVC1f .icon-shape .label{text-align:center;}#mermaid-svg-6Bvbc7MrxzTUVC1f .node.clickable{cursor:pointer;}#mermaid-svg-6Bvbc7MrxzTUVC1f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .arrowheadPath{fill:#333333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Bvbc7MrxzTUVC1f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6Bvbc7MrxzTUVC1f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Bvbc7MrxzTUVC1f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster text{fill:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f .cluster span{color:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f 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-6Bvbc7MrxzTUVC1f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6Bvbc7MrxzTUVC1f rect.text{fill:none;stroke-width:0;}#mermaid-svg-6Bvbc7MrxzTUVC1f .icon-shape,#mermaid-svg-6Bvbc7MrxzTUVC1f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Bvbc7MrxzTUVC1f .icon-shape p,#mermaid-svg-6Bvbc7MrxzTUVC1f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6Bvbc7MrxzTUVC1f .icon-shape .label rect,#mermaid-svg-6Bvbc7MrxzTUVC1f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Bvbc7MrxzTUVC1f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6Bvbc7MrxzTUVC1f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6Bvbc7MrxzTUVC1f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 A
写 volatile flag 为 false
建立 happens before 关系
线程 B 后续读取 flag
看到 false 或更晚的新值
注意这里有两个重点:
- 规则针对的是同一个
volatile字段。 - 它解决的是"看不看得见",不是"多个操作能不能合成一个整体"。
2.2 有序性:写 volatile 前的普通写,不能被随便挪到后面
第二层作用是有序性。简单说,volatile 写像一次发布,volatile 读像一次获取。
看一个配置发布的例子:
java
class ConfigCenter {
private Map<String, String> configMap;
private volatile boolean ready;
public void load() {
Map<String, String> local = readFromDisk();
configMap = local;
ready = true;
}
public String get(String key) {
if (!ready) {
return null;
}
return configMap.get(key);
}
}
当线程 A 执行 ready = true 之前,它已经把 configMap 设置好了。线程 B 读到 ready == true 之后,能看到线程 A 在写 ready 之前做过的那些写入。
这个场景里,ready 不只是一个布尔值,它更像一个"发布完成"的信号。
#mermaid-svg-oGiMxLmGxQSlRtRH{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-oGiMxLmGxQSlRtRH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oGiMxLmGxQSlRtRH .error-icon{fill:#552222;}#mermaid-svg-oGiMxLmGxQSlRtRH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oGiMxLmGxQSlRtRH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oGiMxLmGxQSlRtRH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oGiMxLmGxQSlRtRH .marker.cross{stroke:#333333;}#mermaid-svg-oGiMxLmGxQSlRtRH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oGiMxLmGxQSlRtRH p{margin:0;}#mermaid-svg-oGiMxLmGxQSlRtRH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster-label text{fill:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster-label span{color:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster-label span p{background-color:transparent;}#mermaid-svg-oGiMxLmGxQSlRtRH .label text,#mermaid-svg-oGiMxLmGxQSlRtRH span{fill:#333;color:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH .node rect,#mermaid-svg-oGiMxLmGxQSlRtRH .node circle,#mermaid-svg-oGiMxLmGxQSlRtRH .node ellipse,#mermaid-svg-oGiMxLmGxQSlRtRH .node polygon,#mermaid-svg-oGiMxLmGxQSlRtRH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oGiMxLmGxQSlRtRH .rough-node .label text,#mermaid-svg-oGiMxLmGxQSlRtRH .node .label text,#mermaid-svg-oGiMxLmGxQSlRtRH .image-shape .label,#mermaid-svg-oGiMxLmGxQSlRtRH .icon-shape .label{text-anchor:middle;}#mermaid-svg-oGiMxLmGxQSlRtRH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oGiMxLmGxQSlRtRH .rough-node .label,#mermaid-svg-oGiMxLmGxQSlRtRH .node .label,#mermaid-svg-oGiMxLmGxQSlRtRH .image-shape .label,#mermaid-svg-oGiMxLmGxQSlRtRH .icon-shape .label{text-align:center;}#mermaid-svg-oGiMxLmGxQSlRtRH .node.clickable{cursor:pointer;}#mermaid-svg-oGiMxLmGxQSlRtRH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oGiMxLmGxQSlRtRH .arrowheadPath{fill:#333333;}#mermaid-svg-oGiMxLmGxQSlRtRH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oGiMxLmGxQSlRtRH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oGiMxLmGxQSlRtRH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGiMxLmGxQSlRtRH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oGiMxLmGxQSlRtRH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGiMxLmGxQSlRtRH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster text{fill:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH .cluster span{color:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH 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-oGiMxLmGxQSlRtRH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oGiMxLmGxQSlRtRH rect.text{fill:none;stroke-width:0;}#mermaid-svg-oGiMxLmGxQSlRtRH .icon-shape,#mermaid-svg-oGiMxLmGxQSlRtRH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGiMxLmGxQSlRtRH .icon-shape p,#mermaid-svg-oGiMxLmGxQSlRtRH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oGiMxLmGxQSlRtRH .icon-shape .label rect,#mermaid-svg-oGiMxLmGxQSlRtRH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGiMxLmGxQSlRtRH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oGiMxLmGxQSlRtRH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oGiMxLmGxQSlRtRH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 A 读取配置
写入 configMap
写 volatile ready 为 true
线程 B 读取 ready 为 true
线程 B 安全读取 configMap 引用
这里最好别把话说过头。volatile 能保证引用发布的可见性,但如果 configMap 后面还被多个线程继续修改,那又是另一个并发问题。更稳的做法是发布一个不可变快照,比如新建一个 Map 后再整体替换引用。
3. volatile 不保证什么
volatile 最容易被误用的地方,是把"可见"误解成"线程安全"。
3.1 count++ 不是原子操作
很多人会写:
java
class Counter {
private volatile int count = 0;
public void increase() {
count++;
}
}
这段代码仍然不是线程安全的。因为 count++ 至少包含三步:
text
读取 count
加一
写回 count
两个线程可以同时读到 0,然后都写回 1。最后你明明加了两次,结果只涨了一次。
volatile count ThreadB ThreadA volatile count ThreadB ThreadA #mermaid-svg-OpxgqoLTTutFzXJY{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-OpxgqoLTTutFzXJY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OpxgqoLTTutFzXJY .error-icon{fill:#552222;}#mermaid-svg-OpxgqoLTTutFzXJY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OpxgqoLTTutFzXJY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OpxgqoLTTutFzXJY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OpxgqoLTTutFzXJY .marker.cross{stroke:#333333;}#mermaid-svg-OpxgqoLTTutFzXJY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OpxgqoLTTutFzXJY p{margin:0;}#mermaid-svg-OpxgqoLTTutFzXJY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OpxgqoLTTutFzXJY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-OpxgqoLTTutFzXJY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OpxgqoLTTutFzXJY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-OpxgqoLTTutFzXJY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-OpxgqoLTTutFzXJY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-OpxgqoLTTutFzXJY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-OpxgqoLTTutFzXJY .sequenceNumber{fill:white;}#mermaid-svg-OpxgqoLTTutFzXJY #sequencenumber{fill:#333;}#mermaid-svg-OpxgqoLTTutFzXJY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-OpxgqoLTTutFzXJY .messageText{fill:#333;stroke:none;}#mermaid-svg-OpxgqoLTTutFzXJY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OpxgqoLTTutFzXJY .labelText,#mermaid-svg-OpxgqoLTTutFzXJY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-OpxgqoLTTutFzXJY .loopText,#mermaid-svg-OpxgqoLTTutFzXJY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-OpxgqoLTTutFzXJY .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-OpxgqoLTTutFzXJY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OpxgqoLTTutFzXJY .noteText,#mermaid-svg-OpxgqoLTTutFzXJY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-OpxgqoLTTutFzXJY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OpxgqoLTTutFzXJY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OpxgqoLTTutFzXJY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OpxgqoLTTutFzXJY .actorPopupMenu{position:absolute;}#mermaid-svg-OpxgqoLTTutFzXJY .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-OpxgqoLTTutFzXJY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OpxgqoLTTutFzXJY .actor-man circle,#mermaid-svg-OpxgqoLTTutFzXJY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-OpxgqoLTTutFzXJY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} read 0 1 read 0 2 write 1 3 write 1 4
volatile 保证每次读写都是"看得见"的,但它不能把"读、改、写"这三步打包成一个不可拆的动作。
计数器应该用:
java
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public int increase() {
return count.incrementAndGet();
}
}
AtomicInteger 内部也会用到 volatile 语义,但它还会配合 CAS。也就是说,volatile 解决可见性,CAS 才解决"比较并更新"的原子竞争。
3.2 多字段一致性不是 volatile 能兜住的
再看一个常见错误:
java
class UserState {
private volatile boolean login;
private String userId;
public void login(String id) {
userId = id;
login = true;
}
public String currentUserId() {
return login ? userId : null;
}
}
这段代码在"登录后读取 userId"这种单向发布场景里,看起来还行。但如果后面有退出、切账号、多字段更新,就会变复杂:
java
public void logout() {
login = false;
userId = null;
}
当状态由多个字段共同表达时,读线程可能看到"新 login + 旧 userId",或者"旧 login + 新 userId"这种组合。volatile 没法保证多个字段作为一个整体同时变化。
这种场景更推荐整体不可变快照:
java
final class SessionSnapshot {
final boolean login;
final String userId;
SessionSnapshot(boolean login, String userId) {
this.login = login;
this.userId = userId;
}
}
class SessionStore {
private volatile SessionSnapshot snapshot =
new SessionSnapshot(false, null);
public void login(String userId) {
snapshot = new SessionSnapshot(true, userId);
}
public void logout() {
snapshot = new SessionSnapshot(false, null);
}
public SessionSnapshot current() {
return snapshot;
}
}
这样读线程每次拿到的是一整份状态,而不是几个字段拼出来的半成品。
4. 适合用 volatile 的场景
4.1 停止线程或取消任务的标记位
这是最经典、也最容易理解的场景。
java
class UploadWorker implements Runnable {
private volatile boolean cancelled;
public void cancel() {
cancelled = true;
}
@Override
public void run() {
while (!cancelled) {
uploadOneChunk();
}
releaseTempFile();
}
}
它适合的前提是:任务能定期回到循环里检查这个标记。如果线程卡在 I/O、锁等待、sleep() 或阻塞队列里,单靠 volatile 不一定能及时停下来。
更完整的取消通常会配合:
text
volatile 标记:让循环知道该停
Thread.interrupt:唤醒可中断阻塞
Future.cancel:统一取消任务
协程 Job.cancel:结构化取消
所以面试里可以说:volatile 适合做轻量取消标记,但不是完整取消框架。
4.2 一次性状态发布
比如初始化完成:
java
class ResourceManager {
private Resource resource;
private volatile boolean initialized;
public void init(Context context) {
resource = new Resource(context.getApplicationContext());
initialized = true;
}
public Resource get() {
if (!initialized) {
throw new IllegalStateException("Resource not initialized");
}
return resource;
}
}
这里 initialized 的作用不是保护复杂操作,而是告诉其他线程:"前面的初始化动作已经发布出来了。"
但如果初始化可能被多个线程同时触发,光靠这个布尔值仍然不够。你还要用 synchronized、CAS 或者启动阶段的单线程初始化约束,保证初始化过程本身只发生一次。
4.3 双重检查单例
双重检查单例是 volatile 面试里绕不开的例子。
java
class ImageLoader {
private static volatile ImageLoader instance;
private ImageLoader() {
}
public static ImageLoader getInstance() {
ImageLoader local = instance;
if (local == null) {
synchronized (ImageLoader.class) {
local = instance;
if (local == null) {
local = new ImageLoader();
instance = local;
}
}
}
return local;
}
}
为什么 instance 要加 volatile?因为 new ImageLoader() 不是一个单纯动作,可以粗略拆成:
text
分配内存
执行构造方法
把引用赋值给 instance
如果没有合适的有序性约束,其他线程可能看到一个"引用不为空,但对象还没完整初始化"的状态。volatile 的发布语义可以避免这类重排序带来的问题。
#mermaid-svg-lsvhl14232jhHrFO{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-lsvhl14232jhHrFO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lsvhl14232jhHrFO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lsvhl14232jhHrFO .error-icon{fill:#552222;}#mermaid-svg-lsvhl14232jhHrFO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lsvhl14232jhHrFO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lsvhl14232jhHrFO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lsvhl14232jhHrFO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lsvhl14232jhHrFO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lsvhl14232jhHrFO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lsvhl14232jhHrFO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lsvhl14232jhHrFO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lsvhl14232jhHrFO .marker.cross{stroke:#333333;}#mermaid-svg-lsvhl14232jhHrFO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lsvhl14232jhHrFO p{margin:0;}#mermaid-svg-lsvhl14232jhHrFO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lsvhl14232jhHrFO .cluster-label text{fill:#333;}#mermaid-svg-lsvhl14232jhHrFO .cluster-label span{color:#333;}#mermaid-svg-lsvhl14232jhHrFO .cluster-label span p{background-color:transparent;}#mermaid-svg-lsvhl14232jhHrFO .label text,#mermaid-svg-lsvhl14232jhHrFO span{fill:#333;color:#333;}#mermaid-svg-lsvhl14232jhHrFO .node rect,#mermaid-svg-lsvhl14232jhHrFO .node circle,#mermaid-svg-lsvhl14232jhHrFO .node ellipse,#mermaid-svg-lsvhl14232jhHrFO .node polygon,#mermaid-svg-lsvhl14232jhHrFO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lsvhl14232jhHrFO .rough-node .label text,#mermaid-svg-lsvhl14232jhHrFO .node .label text,#mermaid-svg-lsvhl14232jhHrFO .image-shape .label,#mermaid-svg-lsvhl14232jhHrFO .icon-shape .label{text-anchor:middle;}#mermaid-svg-lsvhl14232jhHrFO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lsvhl14232jhHrFO .rough-node .label,#mermaid-svg-lsvhl14232jhHrFO .node .label,#mermaid-svg-lsvhl14232jhHrFO .image-shape .label,#mermaid-svg-lsvhl14232jhHrFO .icon-shape .label{text-align:center;}#mermaid-svg-lsvhl14232jhHrFO .node.clickable{cursor:pointer;}#mermaid-svg-lsvhl14232jhHrFO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lsvhl14232jhHrFO .arrowheadPath{fill:#333333;}#mermaid-svg-lsvhl14232jhHrFO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lsvhl14232jhHrFO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lsvhl14232jhHrFO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lsvhl14232jhHrFO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lsvhl14232jhHrFO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lsvhl14232jhHrFO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lsvhl14232jhHrFO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lsvhl14232jhHrFO .cluster text{fill:#333;}#mermaid-svg-lsvhl14232jhHrFO .cluster span{color:#333;}#mermaid-svg-lsvhl14232jhHrFO 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-lsvhl14232jhHrFO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lsvhl14232jhHrFO rect.text{fill:none;stroke-width:0;}#mermaid-svg-lsvhl14232jhHrFO .icon-shape,#mermaid-svg-lsvhl14232jhHrFO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lsvhl14232jhHrFO .icon-shape p,#mermaid-svg-lsvhl14232jhHrFO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lsvhl14232jhHrFO .icon-shape .label rect,#mermaid-svg-lsvhl14232jhHrFO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lsvhl14232jhHrFO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lsvhl14232jhHrFO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lsvhl14232jhHrFO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
调用 getInstance
instance 是空
直接返回
进入 synchronized
再次判断为空
创建对象
写 volatile instance
不过工程里我不建议为了展示技术感到处写双重检查。Java 可以用静态内部类,Kotlin 可以用 object 或 by lazy。双重检查适合拿来理解 JMM,不一定是业务代码的首选。
4.4 读多写少的配置快照
比如灰度开关、实验配置、网络策略:
java
final class FeatureConfig {
final boolean newHomeEnabled;
final int maxRetryCount;
final String apiHost;
FeatureConfig(boolean newHomeEnabled, int maxRetryCount, String apiHost) {
this.newHomeEnabled = newHomeEnabled;
this.maxRetryCount = maxRetryCount;
this.apiHost = apiHost;
}
}
class FeatureConfigStore {
private volatile FeatureConfig current =
new FeatureConfig(false, 1, "https://api.example.com");
public void update(FeatureConfig newConfig) {
current = newConfig;
}
public FeatureConfig get() {
return current;
}
}
这个写法有两个关键点:
FeatureConfig尽量不可变。- 更新时整体替换引用,而不是在旧对象上改字段。
这样读线程不需要加锁,能拿到某一刻完整的配置快照。它适合读很多、写很少、允许读到"刚才那一版"的场景。
4.5 Android 里后台线程和主线程共享简单状态
比如后台任务需要看一个开关:
java
class PagePreloader {
private volatile boolean pageDestroyed;
public void onDestroy() {
pageDestroyed = true;
}
public void preload() {
ioExecutor.execute(() -> {
if (pageDestroyed) {
return;
}
Data data = loadData();
if (pageDestroyed) {
return;
}
mainHandler.post(() -> {
if (!pageDestroyed) {
render(data);
}
});
});
}
}
这类代码里,volatile 只能解决"后台线程能不能看见页面已经销毁"这件事。它不能替代 Android 的主线程规则。真正更新 UI,仍然要回到主线程,比如用 Handler、Dispatchers.Main、runOnUiThread() 或生命周期感知组件。
如果你已经在用协程,很多时候更推荐用结构化取消:
kotlin
class DemoViewModel : ViewModel() {
fun load() {
viewModelScope.launch {
val data = withContext(Dispatchers.IO) {
loadData()
}
renderState(data)
}
}
}
协程取消比手写一个 volatile cancelled 更系统,因为它能沿着父子 Job 传播。但理解 volatile 仍然有必要,因为你读 Java 并发包、AndroidX 源码、三方库源码时会经常看到它。
5. 源码里为什么经常能看到 volatile
源码里使用 volatile,通常不是为了"加个线程安全魔法",而是为了表达一种状态发布和跨线程观察。
| 源码位置 | volatile 用在什么地方 |
想解决的问题 |
|---|---|---|
FutureTask |
state、runner、waiters |
任务状态、运行线程、等待队列需要被多个线程观察和推进 |
AtomicInteger |
内部 value |
普通读写有 volatile 语义,更新操作再配合 CAS |
AndroidX LiveData |
mData、mPendingData |
后台 postValue() 和主线程派发之间共享最新值 |
以 FutureTask 为例,它有任务状态:新建、完成、异常、取消、中断中、中断完成。调用 get() 的线程、执行任务的线程、取消任务的线程,都会关心这个状态。状态字段如果不可见,isDone()、cancel()、get() 这种 API 就没法可靠协作。
以 LiveData 为例,postValue() 可以在后台线程调用,但真正的 setValue() 要切回主线程。源码里 mPendingData 既被后台线程写,也会被主线程的 runnable 读取和清空。它还会配合同步块和主线程调度一起工作,不是单靠一个 volatile 解决全部问题。
这就是读源码时最重要的判断:看到 volatile,先问它是不是一个跨线程状态点;再看有没有 CAS、锁、队列或主线程调度一起配合。
6. 不适合用 volatile 的场景
6.1 计数器、自增、自减
错误:
java
private volatile int count;
void add() {
count++;
}
正确方向:
java
private final AtomicInteger count = new AtomicInteger();
void add() {
count.incrementAndGet();
}
如果高并发统计特别频繁,可以继续看 LongAdder。它通过分段减少竞争,比所有线程抢一个数更适合热点计数。
6.2 先判断再修改
这种也不安全:
java
private volatile boolean started;
void start() {
if (!started) {
started = true;
doStart();
}
}
两个线程可能同时看到 started == false,然后都进入 doStart()。
应该用锁或者 CAS:
java
private final AtomicBoolean started = new AtomicBoolean(false);
void start() {
if (started.compareAndSet(false, true)) {
doStart();
}
}
判断和修改必须作为一个整体时,volatile 就不够了。
6.3 多个字段必须一起保持一致
比如余额和版本号、登录态和用户信息、缓存对象和过期时间。如果这几个字段必须一起变化,优先考虑:
text
synchronized
ReentrantLock
不可变快照加 volatile 引用
AtomicReference 包一整个状态对象
不要给每个字段都加 volatile,然后期待它们自动组成一个事务。
6.4 等待条件成立
有些人会写忙等:
java
while (!ready) {
}
哪怕 ready 是 volatile,这个循环也会一直占 CPU。等待条件更合适的工具是:
text
CountDownLatch
Condition
wait 和 notify
BlockingQueue
CompletableFuture
协程挂起函数
Flow 或 LiveData
volatile 适合"顺手看一眼状态",不适合"在这里一直干等"。
7. 和 synchronized、Atomic、final 怎么区分
这几个概念经常一起被问。
| 工具 | 解决什么 | 适合场景 | 代价 |
|---|---|---|---|
volatile |
可见性、有序性 | 简单状态标记、引用发布 | 不保证复合操作原子性 |
synchronized |
互斥、可见性、有序性 | 临界区、多字段一致性 | 可能阻塞,粒度要设计好 |
AtomicInteger 等 |
原子更新、可见性 | 计数器、CAS 状态机 | 复杂逻辑可读性会下降 |
final |
构造后不可变语义 | 不可变对象、安全发布辅助 | 字段本身不能再变 |
Handler |
线程切换和消息排队 | Android 主线程通信 | 不等于共享变量锁 |
协程 Job |
结构化取消 | Kotlin 异步任务 | 要遵守协程作用域 |
一句话区分:
text
只需要别人看见一个新状态,用 volatile。
需要一组代码同一时间只能一个线程执行,用 synchronized 或 Lock。
需要无锁地原子更新一个值,用 Atomic。
需要 Android 线程切换,用 Handler 或协程调度器。
需要生命周期内自动取消,用 viewModelScope、lifecycleScope。
8. 面试官为什么喜欢问 volatile
因为它能顺着问出一整串并发基础:
- 你是否知道 JMM。
- 你是否能区分可见性、原子性、有序性。
- 你是否知道
count++为什么不安全。 - 你是否理解双重检查单例。
- 你是否看过
AtomicInteger、FutureTask、LiveData这类源码。 - 你是否能把 Java 并发讲回 Android 场景。
如果只回答"volatile 保证可见性,不保证原子性",面试官大概率会继续追问:那可见性是什么?为什么 count++ 不行?DCL 为什么要加?Android 里哪里见过?
可以按这段口语化回答:
text
volatile 我一般把它理解成轻量级同步字段。
它主要保证两个东西:第一是可见性,一个线程写了 volatile 字段,另一个线程后续读这个字段能看到;第二是有序性,写 volatile 之前的普通写,不会被乱序到它后面让读线程看不到。
但它不保证复合操作原子性,比如 count++ 本质上是读、加、写三步,两个线程仍然可能同时读到旧值,所以计数器要用 AtomicInteger 或加锁。
项目里适合用 volatile 的地方,一般是取消标记、初始化完成标记、读多写少的配置快照引用,或者双重检查单例里的 instance。Android 里它也常出现在一些跨线程状态字段上,比如 LiveData 的 pending data。真正涉及 UI 线程切换、生命周期取消、多个字段一致性时,我不会只靠 volatile,会用 Handler、协程作用域、Atomic 或 synchronized。
这段回答有场景、有边界,也不把 volatile 吹成万能线程安全。
9. 一个判断口诀
写代码时可以先问自己四个问题:
#mermaid-svg-GOKoMrZIHoD4374L{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-GOKoMrZIHoD4374L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GOKoMrZIHoD4374L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GOKoMrZIHoD4374L .error-icon{fill:#552222;}#mermaid-svg-GOKoMrZIHoD4374L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GOKoMrZIHoD4374L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GOKoMrZIHoD4374L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GOKoMrZIHoD4374L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GOKoMrZIHoD4374L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GOKoMrZIHoD4374L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GOKoMrZIHoD4374L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GOKoMrZIHoD4374L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GOKoMrZIHoD4374L .marker.cross{stroke:#333333;}#mermaid-svg-GOKoMrZIHoD4374L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GOKoMrZIHoD4374L p{margin:0;}#mermaid-svg-GOKoMrZIHoD4374L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GOKoMrZIHoD4374L .cluster-label text{fill:#333;}#mermaid-svg-GOKoMrZIHoD4374L .cluster-label span{color:#333;}#mermaid-svg-GOKoMrZIHoD4374L .cluster-label span p{background-color:transparent;}#mermaid-svg-GOKoMrZIHoD4374L .label text,#mermaid-svg-GOKoMrZIHoD4374L span{fill:#333;color:#333;}#mermaid-svg-GOKoMrZIHoD4374L .node rect,#mermaid-svg-GOKoMrZIHoD4374L .node circle,#mermaid-svg-GOKoMrZIHoD4374L .node ellipse,#mermaid-svg-GOKoMrZIHoD4374L .node polygon,#mermaid-svg-GOKoMrZIHoD4374L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GOKoMrZIHoD4374L .rough-node .label text,#mermaid-svg-GOKoMrZIHoD4374L .node .label text,#mermaid-svg-GOKoMrZIHoD4374L .image-shape .label,#mermaid-svg-GOKoMrZIHoD4374L .icon-shape .label{text-anchor:middle;}#mermaid-svg-GOKoMrZIHoD4374L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GOKoMrZIHoD4374L .rough-node .label,#mermaid-svg-GOKoMrZIHoD4374L .node .label,#mermaid-svg-GOKoMrZIHoD4374L .image-shape .label,#mermaid-svg-GOKoMrZIHoD4374L .icon-shape .label{text-align:center;}#mermaid-svg-GOKoMrZIHoD4374L .node.clickable{cursor:pointer;}#mermaid-svg-GOKoMrZIHoD4374L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GOKoMrZIHoD4374L .arrowheadPath{fill:#333333;}#mermaid-svg-GOKoMrZIHoD4374L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GOKoMrZIHoD4374L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GOKoMrZIHoD4374L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GOKoMrZIHoD4374L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GOKoMrZIHoD4374L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GOKoMrZIHoD4374L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GOKoMrZIHoD4374L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GOKoMrZIHoD4374L .cluster text{fill:#333;}#mermaid-svg-GOKoMrZIHoD4374L .cluster span{color:#333;}#mermaid-svg-GOKoMrZIHoD4374L 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-GOKoMrZIHoD4374L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GOKoMrZIHoD4374L rect.text{fill:none;stroke-width:0;}#mermaid-svg-GOKoMrZIHoD4374L .icon-shape,#mermaid-svg-GOKoMrZIHoD4374L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GOKoMrZIHoD4374L .icon-shape p,#mermaid-svg-GOKoMrZIHoD4374L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GOKoMrZIHoD4374L .icon-shape .label rect,#mermaid-svg-GOKoMrZIHoD4374L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GOKoMrZIHoD4374L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GOKoMrZIHoD4374L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GOKoMrZIHoD4374L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
否
是
否
我要不要用 volatile
只是一个线程写状态
考虑锁 原子类 或并发容器
其他线程只是读取状态
是否需要多个字段一致
用不可变快照或锁
是否有自增或先判断再修改
用 Atomic 或 synchronized
可以考虑 volatile
大白话版:
text
一个字段,一个写方,多个读方,只看状态,不做复合更新,volatile 才舒服。
只要出现"多个字段一起变""先判断再修改""自增统计""等待条件""复杂生命周期",就别硬上 volatile。
10. 小结
volatile 的定位其实很窄,但正因为窄,所以用对了很舒服。
它不是锁,也不是线程安全万能药。它解决的是跨线程状态的可见性和发布顺序问题。停止标记、初始化完成标记、双重检查单例、配置快照引用,这些场景都很适合它。计数器、多字段一致性、集合并发修改、等待条件成立,这些场景就别勉强了。
再把开头那句话收回来:volatile 适合"一个线程写状态,其他线程读状态"的轻量同步;它保证可见性和有序性,但不保证 count++ 这种复合操作的原子性。
11. 源码与参考入口
以下入口按 2026-09-30 整理,用来辅助定位规则和源码职责;具体实现请以项目锁定版本为准。
| 主题 | 链接 |
|---|---|
| Java 语言规范:Happens-before 规则 | JLS 17.4.5 Happens-before Order |
Java 语言规范:volatile long 和 double 的原子读写 |
JLS 17.7 Non-Atomic Treatment of double and long |
OpenJDK AtomicInteger |
AtomicInteger.java |
OpenJDK FutureTask |
FutureTask.java |
AndroidX LiveData |
LiveData.java |