Java PrintStream重定向意外行为

时间:2017-07-16 02:52:23

标签: java multithreading logging printstream

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

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();
}

类LogManager切换打印到的默认PrintStream,并保留旧的副本以打印出日志消息。但是,"记录消息 - "并不总是有前缀。虽然,在每次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大小保持在最低限度,不需要包含额外的包,但主要是因为知道我没有使用任何其他人的库来实现我的目标。

1 个答案:

答案 0 :(得分:2)

你有一些竞争条件。

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

  1. 主线程写道“Goodbye,world \ n”
  2. 主要线程集cont = false
  3. LogManager线程看到cont == false并暂停,然后才有机会写入其字节。
  4. 此外,您使用baos.toByteArray(),然后作为单独的操作执行baos.reset()。如果有人在两个动作之间写了什么,会发生什么?它们不会反映在bytes变量中,但reset()会删除它们。

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

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

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

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

    此外,您应该将cont字段设置为volatile,以便在另一个线程中始终可以看到写入。

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

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

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