Search code examples
javamultithreadingstaticvolatile

Unable to understand the workings of volatile fields and how they work with multiple threads when sharing values


I'm working on a program which needs to load the same information from option files in multiple threads. Therefore I made a simple class representation of each file based on an abstract model: (shown below is 1 example of many of these classes)

package OptionManager;

import java.util.Properties;

public class EngineOptions extends AbstractOptions{
    //values
    private static String debugEnabled;
    private static String debugAvgLoadtime;
    private static String showShaderUsed;
    private static String mainLanguage;

    //keys
    public static final String DEBUGENABLED_KEY = "debugEnabled";
    public static final String DEBUGAVGLOADTIME_KEY = "debugAvgLoadtime";
    public static final String SHOWSHADERUSED_KEY = "showShaderUsed";
    public static final String MAINLANGUAGE_KEY = "mainLanguage";

    public static String getProperty(String key) {
        return properties.getProperty(key);
    }

    public static void loadFromFile(String filename) {      
        OptionReader loader = new OptionReader(filename);
        //load properties
        debugEnabled = loader.getProperty(DEBUGENABLED_KEY);
        debugAvgLoadtime = loader.getProperty(DEBUGAVGLOADTIME_KEY);
        showShaderUsed = loader.getProperty(SHOWSHADERUSED_KEY);
        mainLanguage = loader.getProperty(MAINLANGUAGE_KEY);

        properties.put(DEBUGENABLED_KEY, debugEnabled);
        properties.put(DEBUGAVGLOADTIME_KEY, debugAvgLoadtime);
        properties.put(SHOWSHADERUSED_KEY, showShaderUsed);
        properties.put(MAINLANGUAGE_KEY, mainLanguage);
    }
}

Here is the abstract class it uses:

package OptionManager;

import java.util.Properties;

public abstract class AbstractOptions {
    protected static volatile Properties properties;

    public static void setupProperties() {
        properties = new Properties();
    }

    public static void setupProperties(Properties properties) {}

    public static void setProperty(String key, String value) {
        if(properties.getProperty(key) == null) {
            //throw exception.
        }
        properties.setProperty(key, value);
    }

    public static String getProperty(String key) {
        System.out.println(properties);
        return properties.getProperty(key);
    }

    //public static void loadFromFile(String filename) {}
}

I'm able to load the option files in the main thread(the one created by the JVM in order to start the program) and use this system within this thread to get all the options I want. Infact as a test I print the entire list of options in the EngineOptions class every time I access an option giving the following output(when getting the mainLanguage option):

{mainLanguage=language_IT, debugAvgLoadtime=1, debugEnabled=1, showShaderUsed=1} language_IT

However when I try to access the same option inside another thread(created by the main thread and started after I print the output above) I get this:

{} null

This leads me to believe that for each thread the values of my static fields aren't shared. So I found this answer which suggested using 'volatile' to solve the problem. However this didn't work.

Another option is to load option files in every thread but I would like to not do that as the reason I had to load option files multiple times for each thread was the reason I created this system in the first place.

How can I fix this problem and share the values of the option over multiple threads?

EDIT:

How I create a new option list:

OptionHandler.addOptionFile(OptionHandler.ENGINE_OPTION_ID, new EngineOptions(), "EngineOptions");
OptionHandler.loadOptionListFromFile(OptionHandler.ENGINE_OPTION_ID, OptionHandler.ENGINE_OPTION_TYPE);

How I call the option from the thread:

String currentLang = OptionHandler.getProperty(EngineOptions.MAINLANGUAGE_KEY, OptionHandler.ENGINE_OPTION_ID);
        System.out.println(currentLang);

EDIT 2:

package OptionManager;

import java.util.HashMap;
import java.util.Map;

public class OptionHandler {
    private static HashMap<Integer, AbstractOptions> optionList;
    private static HashMap<Integer, String> optionFilename;

    //OptionFIleID's (Starting from 101 to 199)
    public static final int GRAPHIC_OPTION_ID = 101;
    public static final int ENGINE_OPTION_ID = 102;
    public static final int CURRENT_LANGUAGE_ID = 103;

    public static final int GRAPHIC_OPTION_TYPE = 201;
    public static final int ENGINE_OPTION_TYPE = 202;
    public static final int CURRENT_LANGUAGE_TYPE = 203;

    public static void setupOptions() {
        optionList = new HashMap<Integer, AbstractOptions>();
        optionFilename = new HashMap<Integer, String>();
    }

    public static void addOptionFile(int id, AbstractOptions options, String filename) {
        options.setupProperties();
        optionList.put(id, options);
        optionFilename.put(id, filename);
    }

    public static String getProperty(String optionKey, int optionFileID) {
        return optionList.get(optionFileID).getProperty(optionKey);
    }

    public static void loadOptionListFromFile(int id, int type) {
        System.out.println(optionFilename.get(id));
        if(type == GRAPHIC_OPTION_TYPE)
            GraphicOptions.loadFromFile(optionFilename.get(id));
        if(type == ENGINE_OPTION_TYPE)
            EngineOptions.loadFromFile(optionFilename.get(id));
        if(type == CURRENT_LANGUAGE_TYPE)
            CurrentLanguage.loadFromFile(optionFilename.get(id));
    }
}

Solution

  • This is a thread synchronization problem,If you can guarantee that the loading options action in main thread occurs before the read action in other thread,there is no problem using volatile. If you can't,it is a good idea to use thread synchronization api such as CountDownLatch. This is my modification of your code,Focus on OptionHandler class,and my output is correct.

    import java.util.Properties;
    
    public abstract class AbstractOptions {
        protected volatile Properties properties;
    
        public void setupProperties() {
            properties = new Properties();
        }
    
        public void setupProperties(Properties properties) {}
    
        public void setProperty(String key, String value) {
            if(properties.getProperty(key) == null) {
                //throw exception.
            }
            properties.setProperty(key, value);
        }
    
        public String getProperty(String key) {
            System.out.println(properties);
            return properties.getProperty(key);
        }
    
        public abstract void loadFromFile(String filename);
    }
    
    
    public class EngineOptions extends AbstractOptions{
        //values
        private String debugEnabled;
        private String debugAvgLoadtime;
        private String showShaderUsed;
        private String mainLanguage;
    
        //keys
        public static final String DEBUGENABLED_KEY = "debugEnabled";
        public static final String DEBUGAVGLOADTIME_KEY = "debugAvgLoadtime";
        public static final String SHOWSHADERUSED_KEY = "showShaderUsed";
        public static final String MAINLANGUAGE_KEY = "mainLanguage";
    
        //public String getProperty(String key) {
        //return properties.getProperty(key);
        //}
    
        public void loadFromFile(String filename) {
            //OptionReader loader = new OptionReader(filename);
            //load properties
            debugEnabled = "language_IT";//loader.getProperty(DEBUGENABLED_KEY);
            debugAvgLoadtime = "1";//loader.getProperty(DEBUGAVGLOADTIME_KEY);
            showShaderUsed = "1";//loader.getProperty(SHOWSHADERUSED_KEY);
            mainLanguage = "1";//loader.getProperty(MAINLANGUAGE_KEY);
    
            properties.put(DEBUGENABLED_KEY, debugEnabled);
            properties.put(DEBUGAVGLOADTIME_KEY, debugAvgLoadtime);
            properties.put(SHOWSHADERUSED_KEY, showShaderUsed);
            properties.put(MAINLANGUAGE_KEY, mainLanguage);
        }
    }
    
    import java.util.HashMap;
    import java.util.concurrent.CountDownLatch;
    
    public class OptionHandler {
        private static HashMap<Integer, AbstractOptions> optionList;
        private static HashMap<Integer, String> optionFilename;
        private static CountDownLatch sync;
    
        static {
            setupOptions();
        }
    
        //OptionFIleID's (Starting from 101 to 199)
        public static final int GRAPHIC_OPTION_ID = 101;
        public static final int ENGINE_OPTION_ID = 102;
        public static final int CURRENT_LANGUAGE_ID = 103;
    
        public static final int GRAPHIC_OPTION_TYPE = 201;
        public static final int ENGINE_OPTION_TYPE = 202;
        public static final int CURRENT_LANGUAGE_TYPE = 203;
    
        public static void setupOptions() {
            optionList = new HashMap<Integer, AbstractOptions>();
            optionFilename = new HashMap<Integer, String>();
            //initialize
            sync = new CountDownLatch(1);
        }
    
        public static void addOptionFile(int id, AbstractOptions options, String filename) {
            options.setupProperties();
            optionList.put(id, options);
            optionFilename.put(id, filename);
        }
    
        public static String getProperty(String optionKey, int optionFileID) {
            try {
                //await when the property is not ready yet
                sync.await();
            } catch (InterruptedException e) {
                //log("thread was interrupted")
                Thread.currentThread().interrupt();
            }
            return optionList.get(optionFileID).getProperty(optionKey);
        }
    
        public static void loadOptionListFromFile(int id, int type) {
            System.out.println(optionFilename.get(id));
    //        if(type == GRAPHIC_OPTION_TYPE)
    //            GraphicOptions.loadFromFile(optionFilename.get(id));
            if(type == ENGINE_OPTION_TYPE)
                optionList.get(id).loadFromFile(optionFilename.get(id));
    //        if(type == CURRENT_LANGUAGE_TYPE)
    //            CurrentLanguage.loadFromFile(optionFilename.get(id));
            //Notify other threads that the property is ready
            sync.countDown();
        }
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                String currentLang = 
                      OptionHandler.getProperty(EngineOptions.MAINLANGUAGE_KEY, 
                      OptionHandler.ENGINE_OPTION_ID);
                System.out.println(currentLang);
            }).start();
            Thread.sleep(3000);
            OptionHandler.addOptionFile(OptionHandler.ENGINE_OPTION_ID, new EngineOptions(), "EngineOptions");
            OptionHandler.loadOptionListFromFile(OptionHandler.ENGINE_OPTION_ID, OptionHandler.ENGINE_OPTION_TYPE);
        }
    }