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?

2
  • 2
    you might want to look into FileChannel Commented Dec 7, 2011 at 20:47
  • I second Nerdtron's answer. The Java nio FileChannel solution is by far the simplest to implement. Commented Apr 26, 2012 at 18:31

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

5 Comments

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
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é 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
Can we get a source for this statement? I'm having trouble finding any claim on this matter in the official documentation.
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.
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");

1 Comment

Well that is out of scope to the question, I just asked if FileOutputStream.write was synchronized.
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.

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.