Search code examples
javamultithreadingfilewriter

Java - write to same file with FileWriter - introduce multi-threading


EDIT2: I am fully aware of the reason of the two close votes: there is no multi-threading in OP. So I edited the title.

Threading was not in the OP but in the answer I have it.

EDIT:

OK I give up editing my original post and instead, let me ask like this:

This simulator of log, is designed to generate logs of user and device activities. Each line contains multiple keys and values, separated by ;; and in form key=value. Keys are: User-Name, NAS-Identifier, Acct-Input-Octets, Acct-Output-Octets, date, Acct-Status-Type.

User-Name should be random, like user_<random_number>. NAS-Identifier alike. Acct-Input-Octets and Acct-Output-Octets are integers, beginning by 0 and in ascending order; the increased amount is arbitrary. date should be increasing, too. Acct-Status-Type is one of Start, Interim-Update and Stop, representing the start, continuous state and the end of a user session. There should be one Start, and then multiple Interim-Update, of random numbers, and a Stop. The user and nas is the same during one session.

So, it should generate lines like this:

date=lun, 22 ene 2018 18:39:42.052;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Start;;Acct-Input-Octets=0;;Acct-Output-Octets=0;;
date=lun, 22 ene 2018 18:39:43.827;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=0;;Acct-Output-Octets=0;;
date=lun, 22 ene 2018 18:39:44.463;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=268;;Acct-Output-Octets=42;;
date=lun, 22 ene 2018 18:39:43.968;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=428;;Acct-Output-Octets=143;;
date=lun, 22 ene 2018 18:39:44.039;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=519;;Acct-Output-Octets=294;;
date=lun, 22 ene 2018 18:39:46.276;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Stop;;Acct-Input-Octets=727;;Acct-Output-Octets=535;;
...

But, we can see here that the lines of the same user is all gathered, which is not very real. What I want is something like:

User A Start
User B Start
User C Start
User A Interim-Update
User B Stop
User D Start
User C Interim-Update
User A Interim-Update
...

I want to create several threads, say, 4, to compete between each other; one seize the file and write a line, then release it. Then, another thread writes into the file again, one line and quit.


OP

I have this simple Java class to generate a simulation of Radius log:

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

public class RadiusGenerator {
    private static final String LOG_PATH = "radius-simulation.log";
    private static FileWriter writer;
    private static volatile Map<String, String> user_nas; //ensure the newest value read from main heap; use volatile because we read but not write.
    private static final SimpleDateFormat fmt_in = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss.SSS");
    /* keys in db*/
//    private static final String KEY_TIMESTAMP_HOUR = "timestamp_hour";
//    private static final String KEY_USERID = "user_id";
//    private static final String KEY_NASID = "nas_id";
//    private static final String KEY_TYPE = "type";
//    private static final String KEY_BALANCES = "balances";
//    private static final String KEY_INPUT = "input";
//    private static final String KEY_OUTPUT = "output";

    /* fields in tuple */
    private static final String FIELD_DATE = "date";
    private static final String FIELD_USERNAME = "User-Name";
    private static final String FIELD_NAS_IDENTIFIER = "NAS-Identifier";
    private static final String FIELD_METHOD = "Acct-Status-Type";
    private static final String FIELD_BALANCE_INPUT = "Acct-Input-Octets";
    private static final String FIELD_BALANCE_OUTPUT = "Acct-Output-Octets";

    /* methods */
    private static final String METHOD_START = "Start";
    private static final String METHOD_UPDATE = "Interim-Update";
    private static final String METHOD_STOP = "Stop";
    private static String name;
    private static String nas;
    private static String method;
    private static String result;
    private static int input;
    private static int output;
    private static Date now;


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            RadiusGenerator.writer = new FileWriter(new File(LOG_PATH), false);
            System.out.println("Initializing with parameters: number of users: " + args[0] + ", number of NAS: " + args[1]);
            int numUsers = Integer.parseInt(args[0]);
            int numNAS = Integer.parseInt(args[1]);
//            List<String> usernames = new ArrayList<String>();
//            IntStream.range(1, numUsers).forEach(i -> {
//                usernames.add("User_" + i);
//            });
//            List<String> nasNames = new ArrayList<String>();
//            IntStream.range(1, numNAS).forEach(i -> {
//                nasNames.add("NAS_" + i);
//            });


            FileWriter writer = new FileWriter(new File(LOG_PATH));
            try {
                for (int i=0; i < generateRandom(20); i++) {
                    int indexUser = generateRandom(numUsers-1);
                    name = "User_" + String.valueOf(indexUser);
                    int indexNas = generateRandom(numNAS-1);
                    nas = "NAS_" + String.valueOf(indexNas);
                    user_nas = new HashMap<String, String>();
                    method = "";
                    result = "";
                    now = new Date();
                    input = 0;
                    output = 0;

                    //first line
                    method = METHOD_START;
                    user_nas.put(name, nas);
                    formLine();
                    //update
                    method = METHOD_UPDATE;
                    do {
                        if (generateRandom(10) == 9) {
                            method = "Stop";
                        }
                        formLine();
                        input += generateRandom(300);
                        output += generateRandom(250);
                    } while (!method.equals(METHOD_STOP));
                    //stop
                    user_nas.remove(name);
                    input = 0;
                    output = 0;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            writer.write(result);
            writer.flush();
            writer.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.out.println("The paths is not correct. ");
            e.printStackTrace();
        } catch (NumberFormatException e1) {
            System.out.println("The parameters coming in have wrong formats. Double-check them!");
            e1.printStackTrace();
        }
    }


    private static String concat(String a, String b) {
        return a + "=" + b + ";;";
    }

    private static int generateRandom(int range) {
        return (int) (Math.random() * range);
    }

    private static void formLine() throws IOException {
        result += concat(FIELD_DATE, fmt_in.format(new Date(now.getTime() + (long)generateRandom(5000)))); //randomly add 0-5 seconds
        result += concat(FIELD_USERNAME, name);
        result += concat(FIELD_NAS_IDENTIFIER, nas);
        result += concat(FIELD_METHOD, method);
        result += concat(FIELD_BALANCE_INPUT, String.valueOf(input));
        result += concat(FIELD_BALANCE_OUTPUT, String.valueOf(output));
        result += System.lineSeparator();
    }
}

Which generates output like this:

date=lun, 22 ene 2018 18:39:42.052;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Start;;Acct-Input-Octets=0;;Acct-Output-Octets=0;;
date=lun, 22 ene 2018 18:39:43.827;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=0;;Acct-Output-Octets=0;;
date=lun, 22 ene 2018 18:39:44.463;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=268;;Acct-Output-Octets=42;;
date=lun, 22 ene 2018 18:39:43.968;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=428;;Acct-Output-Octets=143;;
date=lun, 22 ene 2018 18:39:44.039;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=519;;Acct-Output-Octets=294;;
date=lun, 22 ene 2018 18:39:46.276;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=727;;Acct-Output-Octets=535;;
date=lun, 22 ene 2018 18:39:46.126;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=810;;Acct-Output-Octets=694;;
date=lun, 22 ene 2018 18:39:43.908;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=888;;Acct-Output-Octets=848;;
date=lun, 22 ene 2018 18:39:41.839;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=1007;;Acct-Output-Octets=1078;;
date=lun, 22 ene 2018 18:39:42.163;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=1127;;Acct-Output-Octets=1323;;
date=lun, 22 ene 2018 18:39:43.395;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=1367;;Acct-Output-Octets=1543;;
date=lun, 22 ene 2018 18:39:44.430;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=1487;;Acct-Output-Octets=1656;;
date=lun, 22 ene 2018 18:39:44.760;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Interim-Update;;Acct-Input-Octets=1529;;Acct-Output-Octets=1688;;
date=lun, 22 ene 2018 18:39:43.329;;User-Name=User_703;;NAS-Identifier=NAS_0;;Acct-Status-Type=Stop;;Acct-Input-Octets=1561;;Acct-Output-Octets=1746;;

We note that the timestamps are not correctly ordered; I think it is because multiple threads are writing simultaneously to this file. What we want is that the lines are strictly ordered by timestamps(when writing), but START of each user should come first, then UPDATE and at last STOP, to be like a real log.

The topic of synchronization in Java is always a pain. If someone is good at this, please help to point out which block or what object should be locked here. If some good references, better; meanwhile I will be studying this again back home..


Solution

  • Thanks for all the comments, due to your help I have found some problem in my code and I fixed them. Threading was not in the OP but in this answer I have it.

    Judging from the outcome, I believe that:

    • when accessing the same file, writing the line is atomic: once some thread seizes the file, other threads cannot interfere until the owner completes writing his line, for that none of the lines written is composed by content from two threads.
    • when one thread completes the line, it releases the file and immediately, other waiting threads can grab the file and writes his line. Here is a competition, but no condition race, which results in inconsistency.

    So I guess FileWriter is thread-safe?

    The final working code is like:

    • create multiple Runnable to write to the same file
    • each Runnable opens the file and write a line, immediately does a flush(), without close(). The date to write is the current time. Between each writing I add some interval by Thread.sleep(xxx).

    Now I use 4 parameters of launch:

    100
    10
    30
    D:\temp\radius-simulator.log
    

    The class now becomes:

    import java.io.File;
    import java.io.FileWriter;
    import java.io.IOException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.ThreadLocalRandom;
    
    /**
     * A generator simulative of Radius log. Generate lines of log and save to a file. 
     * The parameters expected, in order, are: 
     *  - Number of users, integer(will be number of threads running simultaneously)
     *  - Number of NAS, integer
     *  - Number of seconds to sleep between the previous Stop and the next Start of each thread, integer
     *  - The full path of log file, string (the app in Windows has right to create file, but not the parent folder,
     *      so be sure to create the parent folder first)
     *  
     *
     */
    public class RadiusSimulator implements Runnable {
        private static File log;
        private static volatile Map<String, String> user_nas; //ensure the newest value read from main heap; use volatile because we read but not write.
        private static final SimpleDateFormat fmt_in = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss.SSS");
    
        /* fields in tuple */
        private static final String FIELD_DATE = "date";
        private static final String FIELD_USERNAME = "User-Name";
        private static final String FIELD_NAS_IDENTIFIER = "NAS-Identifier";
        private static final String FIELD_METHOD = "Acct-Status-Type";
        private static final String FIELD_BALANCE_INPUT = "Acct-Input-Octets";
        private static final String FIELD_BALANCE_OUTPUT = "Acct-Output-Octets";
    
        /* methods */
        private static final String METHOD_START = "Start";
        private static final String METHOD_UPDATE = "Interim-Update";
        private static final String METHOD_STOP = "Stop";
    
        /* parameters */
        private static int numUsers;
        private static int numNAS;
        private static int sleepTime;
    
        private String username;
        private String nas;
        private String method;
        private String result;
        private int input;
        private int output;
    
        private FileWriter writer;
        private int status;
        private boolean stop;
        private String name;
    
    
        public RadiusSimulator(String name) {
            this.name = name;
            this.method = METHOD_START;
            status = 0;
            stop = false;
            // just open the file without appending, to clear content
            try {
                writer = new FileWriter(log, false);
                writer.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
        }
    
    
        public static void main(String[] args) {
            try {
                System.out.println("/**\r\n" + 
                        " * A simulated generator of Radius log. Generate lines of log and save to a file. \r\n" + 
                        " * The parameters expected, in order, are: \r\n" + 
                        " *  - Number of users, integer(will be number of threads running simultaneously)\r\n" + 
                        " *  - Number of NAS, integer\r\n" + 
                        " *  - Number of maximum of seconds to sleep between the previous Stop and the next Start of each thread, integer(>60)\r\n" + 
                        " *  - The full path of log file, string (be sure to create the parent folder first)\r\n" + 
                        " *  \r\n" + 
                        " * (require Java 1.7 or above) \r\n\"" +
                        " *  \r\n" + 
                        " *\r\n" + 
                        " */");
    
                System.out.println("Initializing with parameters: number of users: " + args[0] + ", number of NAS: " + args[1]);
                numUsers = Integer.parseInt(args[0]);
                numNAS = Integer.parseInt(args[1]);
                sleepTime = Integer.parseInt(args[2]);
                log = new File(args[3]);
                for (int i=0; i<numUsers; i++) {
                    new Thread(new RadiusSimulator("User_" + i)).start();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
    
    
        }
    
        private int generateRandom(int min, int max) {
            if (max < min) max = min;
            int randomNum = ThreadLocalRandom.current().nextInt(min, max + 1);
            return randomNum;
        }
    
        private static String concat(String a, String b) {
            return a + "=" + b + ";;";
        }
    
        private void formLine() throws IOException {
            result += concat(FIELD_DATE, fmt_in.format(new Date())); //print always the date of now
            result += concat(FIELD_USERNAME, name);
            result += concat(FIELD_NAS_IDENTIFIER, nas);
            result += concat(FIELD_METHOD, method);
            result += concat(FIELD_BALANCE_INPUT, String.valueOf(input));
            result += concat(FIELD_BALANCE_OUTPUT, String.valueOf(output));
            result += System.lineSeparator();
        }
        @Override
        public void run() {
            try {
                writer = new FileWriter(log, true);
            } catch (IOException e1) {
                e1.printStackTrace();
            }
    //        while (!stop) {
            while (true) {
                try {
                    Thread.sleep(generateRandom(0, 3000));
                    if (status == 0) {
                        // stage 1
                        int indexUser = generateRandom(0, numUsers-1);
                        username = "User_" + String.valueOf(indexUser);
                        int indexNas = generateRandom(0, numNAS-1);
                        nas = "NAS_" + String.valueOf(indexNas);
                        user_nas = new HashMap<String, String>();
                        result = "";
                        input = 0;
                        output = 0;
                        //first line
                        user_nas.put(username, nas);
                        formLine();
                        System.out.println(result);
                        writer.write(result);
                        writer.flush();
                        status ++; //next stage
                        result = "";
                    } else if (status == 1) {
                        method = METHOD_UPDATE;
                        input += generateRandom(0, 400);
                        output += generateRandom(0, 500);
                        formLine();
                        System.out.println(result);
                        writer.write(result);
                        writer.flush();
                        // 1 out of 10 of probability to enter stage 3
                        if (generateRandom(0, 10) == 9) {
                            status = 2;
                        }
                        result = "";
                    } else if (status == 2) {
                        method = METHOD_STOP;
                        input += generateRandom(0, 400);
                        output += generateRandom(0, 500);
                        formLine();
                        System.out.println(result);
                        /* write inmediately after constructing the line */
                        writer.write(result);
                        writer.flush();
                        user_nas.remove(username);
                        input = 0;
                        output = 0;
    //                    stop = true;
                        result = "";
                        status = 0; //go back to stage 1
                        Thread.sleep(generateRandom(60*1000, sleepTime*1000));
                    }
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }