java - 一次性初始化 Map 字段的线程安全单例服务类

标签 java multithreading singleton concurrenthashmap

我是开发线程安全方法的新手。我有一个配置服务,实现为单例类,需要线程安全。当服务启动时,一组配置文件被读取并存储在 map 中。这只需要发生一次。我已将 AtomicBoolean 用于 isStarted 状态字段,但我不确定我是否正确执行此操作:

public class ConfigServiceImpl implements ConfigService {
    public static final URL PROFILE_DIR_URL =
           ConfigServiceImpl.class.getClassLoader().getResource("./pageobject_config/");

    private AtomicBoolean isStarted;
    private Map<String,ConcurrentHashMap<String,LoadableConfig>> profiles = new ConcurrentHashMap<>();

    private static final class Loader {
        private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    }

    private ConfigServiceImpl() { }

    public static ConfigServiceImpl getInstance() {
        return Loader.INSTANCE;
    }

    @Override
    public void start() {
        if(!isStarted()) {
            try {
                if (PROFILE_DIR_URL != null) {
                    URI resourceDirUri = PROFILE_DIR_URL.toURI();
                    File resourceDir = new File(resourceDirUri);
                    @SuppressWarnings("ConstantConditions")
                    List<File> files = resourceDir.listFiles() != null ?
                            Arrays.asList(resourceDir.listFiles()) : new ArrayList<>();

                    files.forEach(this::addProfile);
                    isStarted.compareAndSet(false, true);
                }
            } catch (URISyntaxException e) {
                throw new IllegalStateException("Could not generate a valid URI for " + PROFILE_DIR_URL);
            }
        }
    }

    @Override
    public boolean isStarted() {
        return isStarted.get();
    }

    ....
}

我不确定在填充 map 之前是否应该将 isStarted 设置为 true,或者即使这很重要。这种实现在多线程环境中是否相当安全?

更新:

使用 zapl 的建议在私有(private)构造函数中执行所有初始化和 JB Nizet 的建议使用 getResourceAsStream():

public class ConfigServiceImpl implements ConfigService {
    private static final InputStream PROFILE_DIR_STREAM =
            ConfigServiceImpl.class.getClassLoader().getResourceAsStream("./pageobject_config/");

    private Map<String,HashMap<String,LoadableConfig>> profiles = new HashMap<>();

    private static final class Loader {
        private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    }

    private ConfigServiceImpl() {
        if(PROFILE_DIR_STREAM != null) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(PROFILE_DIR_STREAM));
            String line;

            try {
                while ((line = reader.readLine()) != null) {
                    File file = new File(line);
                    ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module());
                    MapType mapType = mapper.getTypeFactory()
                            .constructMapType(HashMap.class, String.class, LoadableConfigImpl.class);

                    try {
                        //noinspection ConstantConditions
                        profiles.put(file.getName(), mapper.readValue(file, mapType));
                    } catch (IOException e) {
                        throw new IllegalStateException("Could not read and process profile " + file);
                    }

                }

                reader.close();
            } catch(IOException e) {
                throw new IllegalStateException("Could not read file list from profile directory");
            }
        }
    }

    public static ConfigServiceImpl getInstance() {
        return Loader.INSTANCE;
    }

    ...
}

最佳答案

最简单的线程安全单例是

public class ConfigServiceImpl implements ConfigService {
    private static final ConfigServiceImpl INSTANCE = new ConfigServiceImpl();
    private ConfigServiceImpl() {
        // all the init code here.
        URI resourceDirUri = PROFILE_FIR_URL.toURI();
        File resourceDir = new File(resourceDirUri);
        ...
    }

    // not synchronized because final field
    public static ConfigService getInstance() { return INSTANCE; }
}

隐藏的构造函数包含所有初始化,并且由于 INSTANCE 是一个 final 字段,Java 语言保证只创建一个实例。而且由于实例创建意味着在构造函数中执行初始化代码,您还可以保证初始化只完成一次。不需要 isStarted()/start()。正确使用复杂的类基本上是不好的做法。无需启动它,您就不会忘记它。

此代码的“问题”是类一加载就进行初始化。有时您想要延迟它,以便它只在有人调用 getInstance() 时发生。为此,您可以引入一个子类来保存 INSTANCE。该子类仅在第一次调用 getInstance 时加载。

通常甚至没有必要强制稍后加载,因为通常情况下,您第一次调用 getInstance 时无论如何都会加载您的类。如果您将该类用于其他用途,它就会变得相关。喜欢保持一些常数。即使您不想初始化所有配置,读取这些也会加载类。


顺便说一句,使用 AtomicBoolean 来一次性初始化的“正确”方法如下:

AtomicBoolean initStarted = new AtomicBoolean();
volatile boolean initDone = false;
Thing thing = null;

public Thing getThing() {
    // only the 1st ever call will do this
    if (initStarted.compareAndSet(false, true)) {
        thing = init();
        initDone = true;
        return thing;
    }

    // all other calls will go here
    if (initDone) {
      return thing;
    } else {
        // you're stuck in a pretty undefined state
        return null;
    }
}
public boolean isInit() {
    return initDone;
}
public boolean needsInit() {
    return !initStarted.get();
}

最大的问题是在实践中你想等到初始化完成而不返回 null 所以你可能永远不会看到这样的代码。

关于java - 一次性初始化 Map 字段的线程安全单例服务类,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32670447/

相关文章:

java - 我的插入排序的实现

Java线程: Query regarding Thread waiting state

java - 在单例模式中,当两个或多个线程同时执行时会发生什么?

c++ - C++中的单例模式

java - Hazelcast 连接到外部地址

java - Spring 框架: which template engine?

java - 这个java阻塞队列变体可能吗?

python - PyQt5 : how to make QThread return data to main thread

java - 数据库行上的锁的范围(简单)

java - DriverManager类是Java中的单例吗?