Search code examples
javaconcurrencyfilewriter

Java concurrent file writing - should fail


I've been testing to write multiple items to a filesystem, fully expecting to get a failure where by one thread overwrites anthers data, or interleaves with the data from another item.

However the following code unexpectedly passes.

Why is the data from one thread not overwriting the data from another thread? All the threads share one writer. Does the code pass because of a JVM implementation detail, or can it genuinely be expected to not mix up individual items.

I've seen some other quests about multiple threads writing to the same file but these were about performance optimizations. Note the import style is just for brevity when posting.

package com.test;

import static org.junit.Assert.assertEquals;

import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;

import org.springframework.boot.CommandLineRunner;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;

public class DiskWriterApplication implements CommandLineRunner {

    public static void main(String[] args) throws Exception {
        new DiskWriterApplication().run(args);
    }

    @Override
    public void run(String... args) throws Exception {
        Path path = Paths.get(System.getProperty("user.home")+"/java-file.txt");
        if (!Files.exists(path)) {
            Files.createFile(path);
        } else {
            Files.delete(path);
            Files.createFile(path);
        }
        BufferedWriter writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"), StandardOpenOption.APPEND);


        Thread[] threads = new Thread[4];

        for (int i=0; i< 4; i++) {
            threads[i] = new Thread(new DataWriter(writer, createDataItems(i)));
        }

        Arrays.asList(threads).forEach(Thread::start);
        Arrays.asList(threads).forEach(t-> {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        });
        writer.close();

        //Verify Lines were written correctly
        ObjectMapper mapper = new ObjectMapper();
        MappingIterator<Data> valueIterator = mapper.readerFor(Data.class).readValues(Files.newInputStream(path));

        Set<String> uniqueItems = new HashSet<>();
        int[] groupItemCount = new int[4];
        while (valueIterator.hasNext())
        {
          Data item = valueIterator.next();

          assertEquals("First Item and second Item should be equal", item.firstValue, item.secondValue);
          assertEquals(10, item.innerObject.size());
          assertEquals(20, item.listValues.size());

          for (int i = 0 ; i< 10; i++) {
              assertEquals(item.firstValue, item.innerObject.get("innerProp"+i));
          }
          for (int i = 0 ; i< 20; i++) {
              assertEquals(item.firstValue, item.listValues.get(i));
          }
          uniqueItems.add(item.firstValue);
          groupItemCount[item.group]++;
        }

        System.out.println("Got " + uniqueItems.size() + " uniqueItems");
        assertEquals("Should be 4000 uniqueItems", 4000, uniqueItems.size());
        assertEquals("Should be 1000 items in group[0]", 1000, groupItemCount[0]);
        assertEquals("Should be 1000 items in group[1]", 1000, groupItemCount[1]);
        assertEquals("Should be 1000 items in group[2]", 1000, groupItemCount[2]);
        assertEquals("Should be 1000 items in group[3]", 1000, groupItemCount[3]);
    }



    private List<Data> createDataItems(int groupNumber) {
        List<Data> items = new ArrayList<>();
        for (int i =0; i<1000; i++) {
            Data item = new Data();
            item.group = groupNumber;
            item.itemNumber = i;
            item.firstValue = "{group" + groupNumber + "item" + i + "}";
            item.secondValue = "{group" + groupNumber + "item" + i + "}";
            for (int j =0; j< 10; j ++) {
                item.addInnerProperty("innerProp"+j , "{group" + groupNumber + "item" + i + "}");
            }
            for (int j=0; j<20; j++) {
                item.addListValue("{group" + groupNumber + "item" + i + "}");
            }
            items.add(item);
        }
        return items;
    }


    private class DataWriter implements Runnable {
        private ArrayList<String> data;
        private PrintWriter writer;

        public DataWriter(BufferedWriter writer, List<Data> items) {
            this.writer = new PrintWriter(writer);
            this.data = new ArrayList<String>();

            ObjectMapper mapper = new ObjectMapper();

            for (Data i : items) {
                try {
                    String stringValue = mapper.writeValueAsString(i);
                    data.add(stringValue);
                } catch (JsonProcessingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }


        @Override
        public void run() {
            System.out.println("Starting batch");
            data.forEach(t -> {
                    writer.println(t);
                    writer.flush();
                    });
            System.out.println("finishing batch");
        }
    }

    public static class Data {
        public int itemNumber;
        public int group;
        @JsonProperty
        private String firstValue;
        @JsonProperty
        private String secondValue;
        @JsonProperty
        private Map<String, String> innerObject = new HashMap<>();
        @JsonProperty
        private List<String> listValues = new ArrayList<>();

        public void addInnerProperty(String key, String value){
            this.innerObject.put(key, value);
        }

        public void addListValue(String value) {
            this.listValues.add(value);
        }

    }
}

Solution

  • As you can see in the others threads asking the same thing : Writing a file using multiple threads in java Is writting on file using bufferwriter initialized by filewriter thread safe or not?

    the BufferedWriter is synchronized and thread-safe