Java PrintStream 重定向行为异常

Java PrintStream redirection behaving unexpectedly

当我使用 System.out.println 打印日志消息时,我正在编写一个基本的服务器程序。我写了一个基本的 class 文件,用它来写出日志。如果我要写以下内容:

System.out.println("Hello, world!");
System.out.println("Goodbye, world");

所需的输出将是:

Log message - Hello, world!
Log message - Goodbye, world!

最终发生的事情与预期的输出不符。相反,它输出到以下内容。

Log message - Hello, world!
Goodbye, world!

主要方法的代码:

public static void main(String[] args){
    LogManager.start();
    System.out.println("Hello, world!");
    System.out.println("Goodbye, world!");
    LogManager.stop();
}

class LogManager 切换打印输出的默认 PrintStream,并保留旧版本的副本以打印日志消息。但是,"Log message - " 并不总是带有前缀。虽然,在每次 println 调用之间休眠 2000 毫秒时,输出如下所示。

Log message - Hello, world!
Log message - Goodbye, world!Log message - 

LogManager的代码如下

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;

public class LogManager implements Runnable{

    private final PrintStream ps;
    private final OutputStream out;
    private static boolean cont = true;

    public static void start(){
        OutputStream stdout = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(stdout);
        Thread th = new Thread(new LogManager(System.out, stdout));
        System.setOut(ps);
        th.start();
    }

    public static void stop(){
        cont = false;
    }

    public LogManager(PrintStream std, OutputStream out){
        this.ps = std;
        this.out = out;
    }

    @Override
    public void run() {
        ByteArrayOutputStream baos = (ByteArrayOutputStream) out;
        while(true){
            if(!cont) return;
            byte[] bytes = baos.toByteArray();
            if(bytes.length > 0){
                baos.reset();
                ps.print("Log message - " + new String(bytes));
            }
        }
    }
}

有人可以指出我做错了什么吗,将不胜感激。我想远离库,因为我想将我的 JAR 大小保持在最低限度,而不必包含额外的包,尽管主要是因为我知道我没有使用任何其他人的库来实现我正在做的事情。

您有一些竞争条件。

首先,您的程序在 stop() 完成后立即结束。发生这种情况时,可能是在 LogManager 线程有机会看到已写入的新字节之前:

  1. 主线程写入"Goodbye, world\n"
  2. 主线程集cont = false
  3. LogManager 线程看到 cont == false 并在有机会写入其字节之前停止。

此外,您使用 baos.toByteArray(),然后作为单独的操作执行 baos.reset()。如果有人在这两个动作之间写了一些东西会怎样?它们不会反映在 bytes 变量中,但 reset() 会删除它们。

要解决第一个问题,您可以在 return 之前进行最后一次检查。换句话说,如果您将整个 toByteArray()/reset()/println 位重构为方法 readAndPrint(),则 return 语句变为:

if (!cont) {
    readAndPrint(); // one last read to empty the buffer
    return;
} 

要解决第二个问题,你应该在 toByteArray()reset() 的同时锁定 boas(这也会锁定对该流的写入,因为所有读取和写入 ByteArrayOutputStream 是同步的)。这将确保在您执行这两个操作时没有其他人可以写入。

byte[] bytes;
synchronized (baos) {
    bytes = baos.toByteArray();
    baos.reset();
}
if (bytes.length > ) { ...

此外,您应该使 cont 字段可变,以便一个线程中的写入总是在另一个线程中看到。

请注意,以上内容仍然会让您对某些比赛保持开放。例如,如果您有两个 "main" 线程,您可以想象其中一个调用 stop() 而另一个仍在尝试打印消息的场景。解决方案是以某种方式协调它,以便在您调用 stop() 时,所有线程都已完成其日志记录。

多线程是一个非常复杂和微妙的话题,很难通过实验来学习。如果您还没有,我强烈建议您阅读一本书或深入的教程,以深入了解问题和解决问题的方法。

最后,您没有询问输出中的奇怪换行符,但它们很可能是由于您正在使用 PrintStream 被刷新(并因此将其内容写入 BAOS)作为打印前缀的信号,而不是在 bytes 缓冲区中看到换行符之类的东西。如果刷新发生在换行符写入之前,您将看到您所看到的行为。