【问题标题】:Thread-safe singleton service class with one-time initialization of Map field具有 Map 字段一次性初始化的线程安全单例服务类
【发布时间】:2015-09-19 16:16:13
【问题描述】:

我是开发线程安全方法的新手。我有一个配置服务,实现为单例类,需要是线程安全的。当服务启动时,会读取一组配置文件并将其存储在映射中。这只需要发生一次。我已将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();
    }

    ....
}

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

更新:

使用 zapl 的建议在私有构造函数中执行所有初始化和 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;
    }

    ...
}

【问题讨论】:

  • 那么,所有调用者都必须先调用start(),然后才能调用任何其他方法?为什么不将初始化代码放在 getInstance() 方法中,以确保您获得的实例总是被初始化?另外,isStarted() 的意义何在,因为两个并行调用 start() 的线程都将读取文件并填充映射?您是否考虑过使用依赖注入框架来避免单反模式?
  • @JB Nizet 他们可以,但不一定必须这样做。他们可以首先调用“isStarted()”,如果值为 false,则尝试启动服务。我不能确切地知道他们是否会做一个或另一个,所以我不得不假设他们可以做任何一个。不过,我喜欢您将所有这些都放在 getInstance() 方法中的想法。我想那会很安全。
  • 只要是同步的,就可以。或者你可以使用单例持有者成语:en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  • 您愿意发布这个作为他问题的答案吗?

标签: java multithreading singleton concurrenthashmap


【解决方案1】:

最简单的线程安全单例是

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

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

此代码的“问题”是加载类后立即进行初始化。你有时想推迟它,所以它只会在有人打电话给getInstance() 时发生。为此,您可以引入一个子类来保存INSTANCE。该子类仅由getInstance 的第一次调用加载。

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


顺便说一句,使用 AtomicBoolean 进行 1 次初始化的“正确”方式类似于:

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,所以你可能永远不会看到这样的代码。

【讨论】:

  • 感谢您提供此信息。当我确实需要这些 Atomic 数据类型时,这个答案将非常有用。我已经更新了我的问题以显示您建议的实现——所有初始化都发生在构造函数中。一旦我有机会实际测试这段代码,我会接受这个作为答案。
  • @JasonDiplomat synchronized 会在每次调用 getInstance() 时增加一些开销,确保访问最终字段是线程安全的,而没有该开销。
【解决方案2】:

您的代码并不是真正的线程安全,因为两个线程可能同时调用 start() 并同时读取文件并填充映射。

使用起来也很不愉快,因为你的单例的调用者将不断地检查(或可能错误地假设)单例已经启动,或者在调用任何其他方法之前调用 start() 以确保它已启动。

我会设计成getInstance() 总是返回一个初始化的实例。确保getInstance() 已同步以避免两个线程同时初始化实例。或使用initialization on demand holder idiom。或者更好的是,不要使用使代码难以进行单元测试的单例反模式,而是使用依赖注入框架。

【讨论】:

  • 您是否有一个示例链接,说明我将如何通过单例实例以外的其他方式执行此操作?我的目标是将配置文件的读取限制为一次。
  • 你的意思是,依赖注入的介绍?您可以阅读github.com/google/guice/wiki/Motivation 的动机部分。
  • 感谢您的链接。因此,我发布了对原始问题的更新——这是否是确保配置文件被读取一次的线程安全实现?
  • 我真的不明白你的装载机的意义。 if 条件不正确,因为您执行的初始化是 isStarted 为真。如果 Maps 应该是只读的,则在初始化完成后,它们不需要并发。您应该将它们包装到不可修改的地图中,以保证它们永远不会被修改。您不需要 AtomicBoolean,因为该变量只能从同步方法访问。一个简单的布尔值就足够了。
  • 另一个不相关的大问题是,一旦您的代码被打包到 jar 或 war 文件中,您的代码就会失败。不要假设 ClassLoader 加载的资源是文件。他们不是。它们只是可读资源,从文件、jar 文件条目或war 文件条目加载。使用 getResourceAsStream()。
猜你喜欢
  • 2012-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-19
  • 1970-01-01
  • 2021-09-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多