在学习 Java 并发编程时,很多同学会发现一个有趣的现象:明明没有用 volatile 或 synchronized,仅仅在循环里加一行 System.out.println(),原本因可见性问题导致的死循环就突然结束了。这背后的原因是什么?能不能用这种方式解决可见性问题?本文带你彻底搞懂。
一、先看一个典型的可见性问题案例
以下代码中,主线程修改 run 变量的值,但子线程可能永远无法感知到,导致死循环:
public class VisibilityDemo {
// 未加 volatile,存在可见性问题
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
// 子线程循环执行
Thread t = new Thread(() -> {
while (run) {
// 空循环,无任何操作
}
});
t.start();
// 主线程休眠1秒后修改run值
Thread.sleep(1000);
run = false;
System.out.println("主线程已将run置为false");
}
}
现象:程序不会退出,子线程的 while 循环会一直执行。
原因:JVM 为了优化性能,子线程会将 run 变量缓存到工作内存中,主线程修改主内存的 run 后,子线程无法感知到(可见性问题)。
二、神奇的变化:加一行System.out.println()就好了
如果在子线程的循环中添加 System.out.println(),程序会正常退出:
public class VisibilityDemo {
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 仅添加这一行,问题就解决了
System.out.println();
}
});
t.start();
Thread.sleep(1000);
run = false;
System.out.println("主线程已将run置为false");
}
}
为什么仅仅一行打印语句就能解决可见性问题?这不是 println 的“魔法”,而是其底层实现的副作用。
三、底层原理:println() 中藏着synchronized
打开 JDK 源码,查看 java.io.PrintStream 类的 println() 方法:
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
private void newLine() {
try {
synchronized (this) {
ensureOpen();
textOut.newLine();
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
核心结论:System.out.println() 底层调用了 synchronized 同步块,而 synchronized 遵循 Java 内存模型(JMM)的监视器锁规则:
对应到案例中:
- 子线程每次执行 println() 时,都会进入 synchronized 块,强制从主内存读取 run 变量;
- 主线程修改 run = false 后,子线程下次执行 println() 时就能读到最新值,从而退出循环。
重要提醒:
println() 解决可见性问题,是依赖 synchronized 的“副作用”,而非其设计目的。synchronized解决可见性问题见:https://blog.csdn.net/weixin_68315058/article/details/158742896?sharetype=blogdetail&sharerId=158742896&sharerefer=PC&sharesource=weixin_68315058&spm=1011.2480.3001.8118
四、为什么绝对不能用println()解决可见性问题?
虽然 println() 能“临时解决”问题,但在生产环境中使用会带来三大致命问题:
五、正确解决可见性问题的方案(生产环境必用)
针对状态标志类的可见性问题,推荐两种标准方案:
方案1:使用 volatile 关键字(最优)
volatile 是 JVM 提供的轻量级同步机制,专门解决可见性和禁止指令重排,无锁开销:
public class VisibilityDemo {
// 加 volatile 保证可见性
private static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 空循环即可
}
});
t.start();
Thread.sleep(1000);
run = false;
System.out.println("主线程已将run置为false");
}
}
方案2:使用 synchronized 显式同步
若涉及复合操作(如读写结合),用 synchronized 包裹所有对共享变量的访问,同时保证原子性和可见性:
public class VisibilityDemo {
private static boolean run = true;
// 定义锁对象
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
synchronized (lock) {
if (!run) {
break;
}
}
}
});
t.start();
Thread.sleep(1000);
synchronized (lock) {
run = false;
}
System.out.println("主线程已将run置为false");
}
}
六、方案对比:println() vs volatile vs synchronized
| System.out.println() | 是(副作用) | 差(I/O + 锁) | 低(依赖实现) | 仅调试临时使用 |
| volatile | 是(设计目的) | 优(无锁) | 高(JVM 规范保证) | 状态标志、简单变量可见性 |
| synchronized | 是(设计目的) | 中(有锁但优化好) | 高(JVM 规范保证) | 复合操作、原子性+可见性 |
最佳实践:
1. 简单状态标志用 volatile;
2. 复合操作(如 i++)用 synchronized 或原子类(AtomicXXX);
3. 绝对不要用 System.out.println() 解决并发问题。
七、总结
1. System.out.println() 解决可见性问题的本质是:底层 synchronized 同步块触发了主内存与工作内存的同步;
2. println() 是“治标不治本”的临时方案,存在性能差、不可靠、语义混淆等问题;
3. 生产环境必须使用 volatile(轻量)或 synchronized(通用)明确保证可见性,这是符合 JMM 规范的正确做法。
Java 并发编程的核心是理解内存模型和同步语义,切勿依赖“巧合”的副作用,只有掌握底层原理,才能写出健壮的并发代码。




