前言
笔者在写一个小 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()
在笔者看来要分成两部分:
- System.out;
- 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。