欢迎光临
我们一直在努力

为什么System.out.println()能解决可见性问题?

在学习 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() 能“临时解决”问题,但在生产环境中使用会带来三大致命问题:

  • 不可靠性:依赖 PrintStream 的具体实现,若 JDK 版本/虚拟机实现变更(如移除 synchronized),该效果会直接消失;
  • 性能损耗:println() 包含同步锁 + 磁盘/控制台 I/O 操作,频繁调用会导致程序性能急剧下降(远高于 volatile);
  • 语义混淆:打印操作的核心职责是输出日志,用它实现并发同步,代码逻辑晦涩,违反“单一职责”原则。
  • 五、正确解决可见性问题的方案(生产环境必用)

    针对状态标志类的可见性问题,推荐两种标准方案:

    方案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 并发编程的核心是理解内存模型和同步语义,切勿依赖“巧合”的副作用,只有掌握底层原理,才能写出健壮的并发代码。

    赞(0)
    未经允许不得转载:171主机测评 » 为什么System.out.println()能解决可见性问题?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址