一次性初始化 Map 字段的线程安全单例服务 class
Thread-safe singleton service class with one-time initialization of Map field
我是开发线程安全方法的新手。我有一个配置服务,实现为 Singleton class,需要线程安全。当服务启动时,一组配置文件被读取并存储在地图中。这只需要发生一次。我已将 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() 并同时读取文件和填充地图。
使用起来也很不愉快,因为你的单例的调用者将不断地检查(或可能错误地假设)单例已经启动,或者在调用任何其他方法之前调用 start() 以确保它已启动.
我会这样设计 getInstance()
总是 returns 初始化实例。确保 getInstance()
是同步的,以避免两个线程同时初始化实例。或者使用initialization on demand holder idiom。或者更好的是,不要使用单例反模式,这会使代码难以进行单元测试,而是使用依赖注入框架。
最简单的线程安全单例是
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 语言保证您只创建 1 个实例。而且由于实例创建意味着在构造函数中执行初始化代码,您还可以保证初始化只完成一次。不需要 isStarted()
/start()
。 classes 的正确使用很复杂,这基本上是一种不好的做法。不用开始就不会忘记。
这段代码的"problem"是class一加载就初始化。您有时想延迟它,以便它只在有人呼叫 getInstance()
时发生。为此,您可以引入一个 subclass 来保存 INSTANCE
。该 subclass 仅由 getInstance
.
的第一次调用加载
通常甚至没有必要强制稍后加载,因为通常情况下,您第一次调用 getInstance
时就是您的 class 加载的时间。如果您将 class 用于其他用途,它就会变得相关。喜欢保持一些常数。即使您不想初始化所有配置,读取这些负载 class。
顺便说一句,"correct" 使用 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();
}
最大的问题是在实践中你想等到初始化完成而不是 return null
所以你可能永远不会看到这样的代码。
我是开发线程安全方法的新手。我有一个配置服务,实现为 Singleton class,需要线程安全。当服务启动时,一组配置文件被读取并存储在地图中。这只需要发生一次。我已将 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() 并同时读取文件和填充地图。
使用起来也很不愉快,因为你的单例的调用者将不断地检查(或可能错误地假设)单例已经启动,或者在调用任何其他方法之前调用 start() 以确保它已启动.
我会这样设计 getInstance()
总是 returns 初始化实例。确保 getInstance()
是同步的,以避免两个线程同时初始化实例。或者使用initialization on demand holder idiom。或者更好的是,不要使用单例反模式,这会使代码难以进行单元测试,而是使用依赖注入框架。
最简单的线程安全单例是
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 语言保证您只创建 1 个实例。而且由于实例创建意味着在构造函数中执行初始化代码,您还可以保证初始化只完成一次。不需要 isStarted()
/start()
。 classes 的正确使用很复杂,这基本上是一种不好的做法。不用开始就不会忘记。
这段代码的"problem"是class一加载就初始化。您有时想延迟它,以便它只在有人呼叫 getInstance()
时发生。为此,您可以引入一个 subclass 来保存 INSTANCE
。该 subclass 仅由 getInstance
.
通常甚至没有必要强制稍后加载,因为通常情况下,您第一次调用 getInstance
时就是您的 class 加载的时间。如果您将 class 用于其他用途,它就会变得相关。喜欢保持一些常数。即使您不想初始化所有配置,读取这些负载 class。
顺便说一句,"correct" 使用 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();
}
最大的问题是在实践中你想等到初始化完成而不是 return null
所以你可能永远不会看到这样的代码。