无法理解可变字段的工作原理以及它们在共享值时如何与多个线程一起工作

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

我正在开发一个需要从多个线程中的选项文件加载相同信息的程序。因此,我根据抽象模型对每个文件做了一个简单的 class 表示:(下面显示的是许多 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);
    }
}

这是它使用的摘要 class:

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) {}
}

我能够在主线程(由 JVM 创建以启动程序的线程)中加载选项文件,并在该线程中使用该系统来获取我想要的所有选项。事实上,作为测试,我每次访问一个选项时都会在 EngineOptions class 中打印整个选项列表,并给出以下输出(当获得 mainLanguage 选项时):

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

然而,当我尝试在另一个线程(由主线程创建并在我打印上面的输出后启动)中访问相同的选项时,我得到这个:

{} null

这让我相信对于每个线程,我的静态字段的值都不会共享。所以我发现 this answer 建议使用 'volatile' 来解决问题。然而,这没有用。

另一个选项是在每个线程中加载选项文件,但我不想这样做,因为我必须为每个线程多次加载选项文件的原因是我首先创建这个系统的原因。

如何解决这个问题并在多个线程中共享选项的值?

编辑:

我如何创建新的选项列表:

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

我如何从线程调用选项:

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

编辑 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));
    }
}

这是一个线程同步问题,如果能保证主线程的加载选项动作发生在其他线程的读取动作之前,使用volatile是没有问题的。如果不能,最好使用线程同步 api 例如 CountDownLatch。 这是我修改你的代码,重点OptionHandler class,我的输出是正确的

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);
    }
}