System.out源码解读——err 和 out 一起用导致的顺序异常Bug

前言

笔者在写一个小 Demo 的过程中,发现了一个奇怪的问题。问题如下:

// 当 flag=true 时打印 a1 ;当 flag=false 时打印 a2。
public static void main(String[] args) {
        boolean flag = false;
        for (int i = 0; i < 10; i++) {
            if (flag) {
                System.out.println("a1");
                flag = false;
            } else {
                System.err.println("a2");
                flag = true;
            }
        }
    }

但当我们运行的时候,会发现控制台输出的顺序并不是我们所想的那样:

而真正我们理想状态下的顺序应该是:(此输出顺序需要我们打断点放慢程序执行速度)

笔者也是第一次注意到这个问题,所以也着实是摸不着头绪。不知道为什么会出现这种问题。那么就来阅读 System.out的源码来分析下这种问题。


一、out 和 err 的定义

JDK文档对两者的解释:

  • out:"标准"输出流。此流已打开并准备接受输出数据。通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标。
  • err:"标准"错误输出流。此流已打开并准备接受输出数据。通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标。按照惯例,此输出流用于显示错误消息,或者显示那些即使用户输出流(变量 out 的值)已经重定向到通常不被连续监视的某一文件或其他目标,也应该立刻引起用户注意的其他信息。

二、源码解读

首先,System.out.println()在笔者看来要分成两部分:

  1. System.out;
  2. out.println();

我们点进去out发现,它其实就是System类的静态成员属性,类型为PrintStream。是一种系统自带的输出流。

    /**
     * "标准"输出流。此流已打开并准备接受输出数据。
     * 通常,此流对应于显示输出或主机环境或用户指定的另一个输出目的地。
     * 如果Console存在,则从字符到字节的转换中使用的编码等效于Console. charset(),
     * 否则等效于stdout.encoding。
     * See the {@code println} methods in class {@code PrintStream}.
     */
    public static final PrintStream out = null;

那我们发现,out这个流被static final修饰,那么我们就可以直接通过类名.属性 (System.out)的方式来访问。同时,System这个类最上方有一块 静态代码块用于初始化资源。会在程序被加载的时候最先调用。

 private static native void registerNatives();

// 程序运行的时候最先加载。registerNatives() 方法是一个本地方法。
// 作用就是通过静态初始化器初始化资源。
static {
    registerNatives();
}

这就是初始化后的效果:


那我们再来看下println方法。println方法在PrintStream这个类中。 提供了多种重构形式:

笔者以我们最常用的println(String x)为例,带大家看看源码。

// 打印一个字符串,然后终止该行
public void println(String x) {
    // 判断是否是由 PrintStream 来调用。
    // 如果使用 System.out 的方法来调用,则永远为 True。
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        // 同步代码块,保证同一时间只有一个线程进入。
        synchronized (this) {
            // 底层也是调用 implWriteln()
            print(x);
            newLine();
        }
    }
}

// 如果判断为 True,则走此方法进行打印
private void writeln(char[] buf) {
    try {
        // 首先判断成员属性 lock 是否已被初始化。在 System.out 时就被静态代码块创建了
        if (lock != null) {
            lock.lock();  // 加锁,保证线程安全
            try {
                implWriteln(buf);
            } finally {
                lock.unlock();  // 解锁
            }
        } else {
            // 如果 lock 没有被初始化,则无法使用此锁。需要使用同步代码块来加锁
            synchronized (this) {
                implWriteln(buf);
            }
        }
    }
    catch (InterruptedIOException x) {
        Thread.currentThread().interrupt();
    }
    catch (IOException x) {
        trouble = true;
    }
}

// 底层真正输出的实现方法
private void implWriteln(char[] buf) throws IOException {
    ensureOpen();  // 检查当前输出流,确保没有被关闭
 
    // textOut 和 charOut是:跟踪文本和字符输出流,以便在不刷新整个流的情况下刷新它们的缓冲区。
    textOut.write(buf);  // 输出字节数组
    textOut.newLine();  // 输出换行符号,帮助我们换行
    // 刷新流,保证不会有元素还在缓存区没输出
    // 底层其实就是判断当前流中的元素是否 = 0,如果 != 0 则调用 write() 方法在输出一次。
    textOut.flushBuffer();
    charOut.flushBuffer();
    if (autoFlush)
        out.flush();
}

问题原因及解决办法

当时阅读完 out 的源码后,我就在思考,会不会是两种不同的输出流导致线程冲突了。于是我使用 setErr 将 err 的输出流重定向为 out。再试一次结果:

java 复制代码
public static void main(String[] args) {
        boolean flag = false;
        for (int i = 0; i < 10; i++) {
            if (flag) {
                System.out.println("a1");
                flag = false;
            } else {
                System.setErr(System.out);  // 就是这句话
                System.err.println("a2");
                flag = true;
        }

    }
 }

我们发现,确实输出顺序异常的问题解决了。那就证明我们的思路没问题。

但还是有问题:System.err打印不出红色。

这个原因我查了一下:System.err.println只能在屏幕上实现打印,即使你重定向了也一样。

总结

System.out.println()的性能并不好。当我们深入分析时,其调用顺序如下 println - > print - > write()+ newLine() 。这个顺序流是Sun / Oracle JDK的实现。write()newLine()都包含一个synchronized块。同步有一点开销,但更多的是添加字符到缓冲区和打印的开销更大。

无论如何请勿使用System.out.println打印日志!

下篇文章将带大家探究为什么 System.out 和 System.err 一起运行会导致顺序异常的 Bug。

相关推荐
学步_技术3 分钟前
Python编码系列—Python组合模式:构建灵活的对象组合
开发语言·python·组合模式
丶白泽21 分钟前
重修设计模式-结构型-桥接模式
java·设计模式·桥接模式
o独酌o27 分钟前
递归的‘浅’理解
java·开发语言
Book_熬夜!30 分钟前
Python基础(六)——PyEcharts数据可视化初级版
开发语言·python·信息可视化·echarts·数据可视化
无问81739 分钟前
数据结构-排序(冒泡,选择,插入,希尔,快排,归并,堆排)
java·数据结构·排序算法
m0_631270401 小时前
高级c语言(五)
c语言·开发语言
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS在线文档管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
2401_858286111 小时前
53.【C语言】 字符函数和字符串函数(strcmp函数)
c语言·开发语言
Flying_Fish_roe1 小时前
Spring Boot-版本兼容性问题
java·spring boot·后端
程序猿进阶1 小时前
如何在 Visual Studio Code 中反编译具有正确行号的 Java 类?
java·ide·vscode·算法·面试·职场和发展·架构