System.out.println 和 System.err.println 乱序

Posted

技术标签:

【中文标题】System.out.println 和 System.err.println 乱序【英文标题】:System.out.println and System.err.println out of order 【发布时间】:2010-12-25 09:29:05 【问题描述】:

我的 System.out.println()System.err.println() 呼叫没有按照我发出的顺序打印到控制台。

public static void main(String[] args) 
    for (int i = 0; i < 5; i++) 
        System.out.println("out");
        System.err.println("err");
    

这会产生:

out
out
out
out
out
err
err
err
err
err

而不是交替使用 outerr。这是为什么呢?

【问题讨论】:

【参考方案1】:

它们是不同的流,在不同的时间刷新。

如果你放

System.out.flush();
System.err.flush();

在您的循环中,它将按预期工作。

为了澄清,输出流被缓存,所以所有的写入都进入这个内存缓冲区。经过一段时间的静默,它们实际上被写出来了。

您写入两个缓冲区,然后在一段时间不活动后,它们都被刷新(一个接一个)。

【讨论】:

这不起作用。将这些刷新命令放入循环后,没有任何变化。使用 Java 1.7。 已经在 Stack Overflow 主题 Java: synchronizing standard out and standard error987654321 上回答了 这个答案实际上是不正确的,因为 System.outSystem.err 应该在每个换行符处自动刷新,这在调用使用 println() 时显然存在。所以这应该没什么区别。 但添加刷新语句会改变行为。奇怪。 我在使用 IntelliJ 时遇到了这个问题。比尔关于包含 System.err.flush() 的建议;写错后解决了我的问题。谢谢@BillK!【参考方案2】:

如果您使用的是 Eclipse 控制台,似乎有两种不同的现象在起作用:如@Gemtastic 所述,一种是 JVM 处理流,另一种是 Eclipse 读取这些流的方式,如@DraganBozanovic 所述。由于我使用的是 Eclipse,@BillK 发布的优雅的 flush()-solution 仅解决了 JVM 问题,是不够的。

我最终为自己编写了一个名为 EclipseTools 的辅助类,其中包含以下内容(以及所需的包声明和导入)。这有点小技巧,但解决了这两个问题:

public class EclipseTools 

    private static List<OutputStream> streams = null;
    private static OutputStream lastStream = null;

    private static class FixedStream extends OutputStream 

        private final OutputStream target;

        public FixedStream(OutputStream originalStream) 
            target = originalStream;
            streams.add(this);
        

        @Override
        public void write(int b) throws IOException 
            if (lastStream!=this) swap();
            target.write(b);
        

        @Override
        public void write(byte[] b) throws IOException 
            if (lastStream!=this) swap();
            target.write(b);
        

        @Override
        public void write(byte[] b, int off, int len) throws IOException 
            if (lastStream!=this) swap();
            target.write(b, off, len);
        

        private void swap() throws IOException 
            if (lastStream!=null) 
                lastStream.flush();
                try  Thread.sleep(200);  catch (InterruptedException e) 
            
            lastStream = this;
        

        @Override public void close() throws IOException  target.close(); 
        @Override public void flush() throws IOException  target.flush(); 
    

    /**
     * Inserts a 200ms delay into the System.err or System.out OutputStreams
     * every time the output switches from one to the other. This prevents
     * the Eclipse console from showing the output of the two streams out of
     * order. This function only needs to be called once.
     */
    public static void fixConsole() 
        if (streams!=null) return;
        streams = new ArrayList<OutputStream>();
        System.setErr(new PrintStream(new FixedStream(System.err)));
        System.setOut(new PrintStream(new FixedStream(System.out)));
    

要使用,只需在代码开头调用一次EclipseTools.fixConsole()

基本上,这会将System.errSystem.out 两个流替换为一组自定义流,这些流只是将其数据转发到原始流,但会跟踪最后写入哪个流。如果写入的流发生更改,例如 System.err.something(...) 后跟 System.out.something(...),它会刷新最后一个流的输出并等待 200 毫秒,让 Eclipse 控制台有时间完成打印。

注意:200ms 只是一个粗略的初始值。如果此代码减少但不能为您消除问题,请将Thread.sleep 中的延迟从 200 增加到更高的值,直到它起作用。或者,如果这种延迟有效但影响了代码的性能(如果您经常交替流),您可以尝试逐渐减少它,直到开始出现错误。

【讨论】:

【参考方案3】:

这是由 JVM 中的一项功能引起的,除非您像 Marcus A. 提供的那样进行破解。要解决此问题并不容易。 .flush() 在这种情况下有效,但解决这个问题的原因要复杂得多。

这里发生了什么?

当您使用 Java 编程时,您并不是直接告诉计算机要做什么,而是告诉 JVM(Java 虚拟机)您希望它做什么。它会这样做,但以更有效的方式。您的代码不是精确的详细说明,在这种情况下,您只需要像 C 和 C++ 中的编译器,JVM 将您的代码作为规范列表,用于它应该优化然后执行的操作。 这就是这里发生的事情。 Java 看到您正在将字符串推送到两个不同的缓冲区流中。最有效的方法是缓冲您希望流输出的所有字符串,然后将其输出。这发生在一次流中,本质上是转换你的代码做这样的事情(注意:伪代码)

for(int i = 0; i < 5; i++) 
    out.add();
    err.add();

out.flush();
err.flush();

因为这样效率更高,所以 JVM 将改为这样做。在循环中添加.flush() 将向JVM 发出信号,表明需要在每个循环中进行刷新,上述方法无法改进。但是如果你为了解释它是如何工作的而忽略了循环,那么 JVM 将重新排序你的代码以最后完成打印,因为这样更有效。

System.out.println("out");
System.out.flush();
System.err.println("err");
System.err.flush();
System.out.println("out");
System.out.flush();
System.err.println("err");
System.err.flush();

这段代码总是会被重新组织成这样的:

System.out.println("out");*
System.err.println("err");*
System.out.println("out");*
System.err.println("err");*
System.out.flush();
System.err.flush();

因为缓冲许多缓冲区只是为了在之后立即刷新它们比缓冲所有要缓冲的代码然后同时刷新它们需要更多的时间。

如何解决

这就是代码设计和架构可能发挥作用的地方;你有点不解决这个问题。要解决此问题,您必须使缓冲打印/刷新、缓冲打印/刷新比缓冲全部然后刷新更有效。这很可能会引诱你进入糟糕的设计。如果如何有序输出对您很重要,我建议您尝试不同的方法。使用.flush() 进行 for 循环是破解它的一种方法,但您仍在破解 JVM 的功能以重新安排和优化您的代码。

*我无法验证您首先添加的缓冲区是否总是首先打印,但很可能会。

【讨论】:

【参考方案4】:

这是bug in Eclipse。 Eclipse 似乎使用单独的线程来读取outerr 流的内容,没有任何同步。

如果编译类并在控制台中执行(使用经典的java &lt;main class name&gt;),则顺序符合预期。

【讨论】:

该错误已在 Eclipse 2019-09 中修复。现在你有了option to synchronize System.out and System.err(默认禁用)。但是,System.err 输出会失去红色,也会变成黑色。【参考方案5】:

两个println 语句由两个不同的线程处理。输出再次取决于您运行代码的环境。 例如,我在 IntelliJ 和命令行中分别执行了以下代码 5 次。

public class Test 
    public static void main(String[] args) 
        for (int i = 0; i < 10; i++) 
            System.out.print("OUT ");
            System.err.print("ERR ");
        
    

这导致以下输出: 命令行

OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR

智能:

ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT 
OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR
ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT 
ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT
OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR 

我猜不同的环境对缓冲区的处理方式不同。 查看这些流实际上由不同线程处理的一种方法是在循环中添加sleep 语句。您可以尝试更改为睡眠设置的值,看看这些值实际上是由不同的线程处理的。

public class Test 
    public static void main(String[] args) 
        for (int i = 0; i < 10; i++) 
            System.out.print("OUT ");
            System.err.print("ERR ");
            try 
                Thread.sleep(500);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

这种情况下的输出结果是

OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR
OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR
ERR OUT ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR ERR OUT ERR OUT 
ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR
OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR 

强制它以相同顺序打印的一种方法是使用.flush(),它对我有用。但似乎并不是每个人都能得到正确的结果。

两个不同线程处理的两个流可能是我们有时看到我们使用的某些库打印的ERROR消息的原因,在我们应该根据顺序看到的一些打印语句之前打印执行。

【讨论】:

【参考方案6】:

我已经使用线程将 System.out 和 System.err 的输出顺序打印为:

    for(int i = 0; i< 5; i++)
        try 
            Thread.sleep(100);
            System.out.print("OUT");
            Thread.sleep(100);
            System.err.print("ERR");
        catch (InterruptedException ex)
            System.out.println(ex.getMessage());
        
    

【讨论】:

【参考方案7】:

特别是在 Eclipse 中,您现在可以在控制台中使用 Eclipse 2019-09 同步标准和错误输出。

Eclipse 控制台视图目前无法确保混合标准和错误输出的显示顺序与运行进程产生的顺序相同。

对于 Java 应用程序,启动配置通用选项卡现在提供合并标准和错误输出的选项。 这可确保标准输出和错误输出以其生成的顺序显示,但同时禁用错误输出的单独着色。

【讨论】:

以上是关于System.out.println 和 System.err.println 乱序的主要内容,如果未能解决你的问题,请参考以下文章

比较运算符

记录一次别人的面试题

输入输出

[java] java中的初始化顺序

java 继承运行结果解释

java数据类型