12

Is it safe to call write on Java FileOutputStream object form multiple threads? Will the output be serialized correctly?

clarification:

In my case the class logger holds a FileOutputStream reference, and multiple threads can call logger write, that formats the output and calls FileOutputStream write.

Should I synchronize my logger write method to warrant that the messages from multiple threads are not mixed?

Udo Held
  • 12,314
  • 11
  • 67
  • 93
José
  • 3,041
  • 8
  • 37
  • 58
  • 2
    you might want to look into [FileChannel](http://docs.oracle.com/javase/6/docs/api/java/nio/channels/FileChannel.html) – Nerdtron Dec 07 '11 at 20:47
  • I second Nerdtron's answer. The Java nio FileChannel solution is by far the simplest to implement. – Akos Cz Apr 26 '12 at 18:31

4 Answers4

8

A file can not be opened more than once in write-mode, so the answer is no.

After seeing your edit, yes you should introduce synchronization into your logger to make sure the stream is accessed only by one thread at a time. Just a suggestion, why don't you go for Log4J? It already handles your use case.

GETah
  • 20,922
  • 7
  • 61
  • 103
  • In my case the class logger holds a FileOutputStream reference, and multiple threads can call logger write, that format the output and calls FileOutputStream write – José Dec 07 '11 at 20:55
  • About log4j, that is part of a library, there is a log interface and the simple implementation just write to a file, applications can still use log4J or others, but i don't want to force this dependency for the simple cases. – José Dec 07 '11 at 21:52
  • @José ah ok. If you don't want to use log4j then that is fine. Just make sure your threads are synchronized when writing to your log file – GETah Dec 07 '11 at 21:55
  • 1
    Can we get a source for this statement? I'm having trouble finding any claim on this matter in the official documentation. – Jasper Jun 19 '14 at 08:22
  • Sorry I have to downvote this answer, since it's IMO wrong. It initially answers different question then asked, then it answers correct question, but provides what seems to be wrong answer - and it gives no evidence, it just states it as fact. – Michal Feb 12 '16 at 11:44
5

Here is a simple implementation of a synchronized logger using the java nio FileChannel. In this example, log messages are limited to 1024 bytes. You can adjust the log message length by changing the BUFFER_SIZE value.

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;

/**
 *  The MyLogger class abstracts the writing of log messages to a file.
 *  This is a synchronized implementation due to the usage of java.nio.channels.FileChannel 
 *  which is used to write log messages to the log file.
 *  
 *  The MyLogger class maintains a HashMap of MyLogger instances per log file.  
 *  The Key is the MD5 hash of the log file path and the Value is the MyLogger instance for that log file.
 *
 */
public final class MyLogger {
    private static final int BUFFER_SIZE = 1024;
    private static final int DIGEST_BASE_RADIX = 16;
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    private static HashMap<String, MyLogger> sLoggerMap;

    private FileChannel mLogOutputChannel;
    private ByteBuffer mByteBuffer;
    private String mLogDir;
    private String mLogFileName;

    /**
     * Private constructor which creates our log dir and log file if they do not already already exist. 
     * If the log file exists, then it is opened in append mode.
     * 
     * @param logDir
     *            The dir where the log file resides
     * @param logFileName
     *            The file name of the log file
     * @throws IOException
     *             Thrown if the file could not be created or opened for writing.
     */
    private MyLogger(String logDir, String logFileName) throws IOException {
        mLogDir = logDir;
        mLogFileName = logFileName;

        // create the log dir and log file if they do not exist
        FileOutputStream logFile;
        new File(mLogDir).mkdirs();

        final String logFilePath = mLogDir + File.separatorChar + mLogFileName;
        final File f = new File(logFilePath);
        if(!f.exists()) {
            f.createNewFile();
        }
        logFile = new FileOutputStream(logFilePath, true);

        // set up our output channel and byte buffer  
        mLogOutputChannel = logFile.getChannel();
        mByteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
    }

    /**
     * Writes the given log message to the log file that is represented by this MyLogger instance. 
     * If the log message could not be written to the log file an error is logged in the System log.
     * 
     * @param logMessage
     *            The log message to write to the log file.
     */
    public void log(String logMessage) {

        // write the log message to the log file
        if (mLogOutputChannel != null) {
            mByteBuffer.put(logMessage.getBytes());
            mByteBuffer.put(LINE_SEPARATOR.getBytes());
            mByteBuffer.flip();
            try {
                mLogOutputChannel.write(mByteBuffer);
                // ensure that the data we just wrote to the log file is pushed to the disk right away
                mLogOutputChannel.force(true);
            } catch (IOException e) {
                // Could not write to log file output channel
                e.printStackTrace();
                return;
            }
        }

        if(mByteBuffer != null) {
            mByteBuffer.clear();
        }
    }

    /**
     * Get an instance of the MyLogger for the given log file. Passing in the same logDir and logFileName will result in the same MyLogger instance being returned.
     * 
     * @param logDir
     *            The directory path where the log file resides. Cannot be empty or null.
     * @param logFileName
     *            The name of the log file Cannot be empty or null.
     * @return The instance of the MyLogger representing the given log file. Null is returned if either logDir or logFilename is null or empty string.
     * @throws IOException
     *             Thrown if the file could not be created or opened for writing.
     */
    public static MyLogger getLog(String logDir, String logFileName) throws IOException {
        if(logDir == null || logFileName == null || logDir.isEmpty() || logFileName.isEmpty()) {
            return null;
        }

        if(sLoggerMap == null) {
            sLoggerMap = new HashMap<String, MyLogger>();
        }

        final String logFilePathHash = getHash(logDir + File.separatorChar + logFileName);
        if(!sLoggerMap.containsKey(logFilePathHash)) {
            sLoggerMap.put(logFilePathHash, new MyLogger(logDir, logFileName));
        }

        return sLoggerMap.get(logFilePathHash);
    }

    /**
     * Utility method for generating an MD5 hash from the given string.
     * 
     * @param path
     *            The file path to our log file
     * @return An MD5 hash of the log file path. If an MD5 hash could not be generated, the path string is returned.
     */
    private static String getHash(String path) {
        try {
            final MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(path.getBytes());
            return new BigInteger(digest.digest()).toString(DIGEST_BASE_RADIX);
        } catch (NoSuchAlgorithmException ex) {
            // this should never happen, but just to make sure return the path string
            return path;
        }
    }
}

This is how you would use it :

MyLogger aLogger = MyLogger.getLog("/path/to/log/dir", "logFilename");
aLogger.log("my log message");
Akos Cz
  • 12,711
  • 1
  • 37
  • 32
  • Well that is out of scope to the question, I just asked if FileOutputStream.write was synchronized. – José Jul 14 '15 at 09:27
2

No. Java does not support streaming to the same stream from multiple threads.

If you want to do use threaded streams, check out this site: http://lifeinide.com/post/2011-05-25-threaded-iostreams-in-java/

He explains things well and has some sample code for a ThreadedOutputStream, which would do what you want.

Lukasz Frankowski
  • 2,955
  • 1
  • 31
  • 32
Jon Egeland
  • 12,470
  • 8
  • 47
  • 62
1

If you want to keep ordering (ie message 1 in the output stream came before message 2) you have to lock the stream. This in turn reduces concurrency. (All threads will be enqueued in the lock's/semaphore's queue and wait there for the stream to become available to them)

If you're interested only in writing to a stream concurrently and don't care about ordering, you can have buffers for each thread. Each thread writes to its own buffer. When the buffer is full it acquires a lock (which may involve waiting for the lock) on the stream and empties its contents into the stream.

Edit: I just realized that, if you care about ordering and still want multi-threading, if you also write the time in the output stream in unix format (as a long). After the stream is flushed onto some other container, the contents can be sorted based on time and you should have an ordered file.

Adrian
  • 5,603
  • 8
  • 53
  • 85