Java取值时如何实现线程安全的HashMap惰性初始化?
How to implement thread-safe HashMap lazy initialization when getting value in Java?
我想实现一个通过字符串值获取枚举对象的实用程序。这是我的实现。
IStringEnum.java
public interface IStringEnum {
String getValue();
}
StringEnumUtil.java
public class StringEnumUtil {
private volatile static Map<String, Map<String, Enum>> stringEnumMap = new HashMap<>();
private StringEnumUtil() {}
public static <T extends Enum<T>> Enum fromString(Class<T> enumClass, String symbol) {
final String enumClassName = enumClass.getName();
if (!stringEnumMap.containsKey(enumClassName)) {
synchronized (enumClass) {
if (!stringEnumMap.containsKey(enumClassName)) {
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
Map<String, Enum> innerMap = new HashMap<>();
EnumSet<T> set = EnumSet.allOf(enumClass);
for (Enum e: set) {
if (e instanceof IStringEnum) {
innerMap.put(((IStringEnum) e).getValue(), e);
}
}
stringEnumMap.put(enumClassName, innerMap);
}
}
}
return stringEnumMap.get(enumClassName).get(symbol);
}
}
我写了一个单元测试来测试它是否适用于多线程情况。
StringEnumUtilTest.java
public class StringEnumUtilTest {
enum TestEnum implements IStringEnum {
ONE("one");
TestEnum(String value) {
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
private String value;
}
@Test
public void testFromStringMultiThreadShouldOk() {
final int numThread = 100;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(numThread);
List<Boolean> resultList = new LinkedList<>();
for (int i = 0; i < numThread; ++i) {
new Thread(() -> {
try {
startLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
resultList.add(StringEnumUtil.fromString(TestEnum.class, "one") != null);
doneLatch.countDown();
}).start();
}
startLatch.countDown();
try {
doneLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
assertEquals(numThread, resultList.stream().filter(item -> item.booleanValue()).count());
}
}
测试结果为:
aaa:null
java.lang.AssertionError:
Expected :100
Actual :98
表示只有一个线程执行这行代码:
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
所以初始化代码应该只由一个线程执行。
奇怪的是,执行这行代码后,某些线程的结果会是null
:
return stringEnumMap.get(enumClassName).get(symbol);
由于没有 NullPointerException,stringEnumMap.get(enumClassName)
必须 return innerMap
的引用。但是为什么调用innerMap
的get(symbol)
后会得到null
?
请帮忙,我整天都快疯了!
stringEnumMap
应该是 ConcurrentHashMap<String, Map<String,Enum>>
,并使用 computeIfAbsent
进行惰性初始化。
尝试移动
如果 (!stringEnumMap.containsKey(enumClassName))
和
return stringEnumMap.get(enumClassName).get(symbol);
进入同步块。
问题出在线路
List<Boolean> resultList = new LinkedList<>();
Note that this implementation is not synchronized.If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.If no such object exists, the list should be "wrapped" using the Collections.synchronizedListmethod. This is best done at creation time, to prevent accidental unsynchronized access to the list:
List list = Collections.synchronizedList(new LinkedList(...));
因为 LinkedList
不是线程安全的,在 add
操作期间可能会发生意外行为。
这导致 resultList
大小小于线程计数,因此预期计数小于结果计数。
要获得正确的结果,请按照建议添加 Collections.synchronizedList
。
虽然您的实施很好,但我建议您遵循 Matt Timmermans 的回答以获得更简单和可靠的解决方案。
ConcurrentMap
界面
正如其他人指出的那样,如果跨线程操作 Map
,您必须考虑并发性。
您可以自己处理并发访问。但没有必要。 Java 带有两个 Map
的实现,它们是为内部处理并发而构建的。这些实现实现了 ConcurrentMap
接口。
ConcurrentSkipListMap
ConcurrentHashMap
第一个按排序顺序维护键,实现NavigableMap
接口。
这是我编写的 table 来展示 Map
与 Java 11.
捆绑在一起的所有实现的特征
您可能会发现 ConcurrentMap
接口的其他第三方实现。
我想实现一个通过字符串值获取枚举对象的实用程序。这是我的实现。
IStringEnum.java
public interface IStringEnum {
String getValue();
}
StringEnumUtil.java
public class StringEnumUtil {
private volatile static Map<String, Map<String, Enum>> stringEnumMap = new HashMap<>();
private StringEnumUtil() {}
public static <T extends Enum<T>> Enum fromString(Class<T> enumClass, String symbol) {
final String enumClassName = enumClass.getName();
if (!stringEnumMap.containsKey(enumClassName)) {
synchronized (enumClass) {
if (!stringEnumMap.containsKey(enumClassName)) {
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
Map<String, Enum> innerMap = new HashMap<>();
EnumSet<T> set = EnumSet.allOf(enumClass);
for (Enum e: set) {
if (e instanceof IStringEnum) {
innerMap.put(((IStringEnum) e).getValue(), e);
}
}
stringEnumMap.put(enumClassName, innerMap);
}
}
}
return stringEnumMap.get(enumClassName).get(symbol);
}
}
我写了一个单元测试来测试它是否适用于多线程情况。
StringEnumUtilTest.java
public class StringEnumUtilTest {
enum TestEnum implements IStringEnum {
ONE("one");
TestEnum(String value) {
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
private String value;
}
@Test
public void testFromStringMultiThreadShouldOk() {
final int numThread = 100;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(numThread);
List<Boolean> resultList = new LinkedList<>();
for (int i = 0; i < numThread; ++i) {
new Thread(() -> {
try {
startLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
resultList.add(StringEnumUtil.fromString(TestEnum.class, "one") != null);
doneLatch.countDown();
}).start();
}
startLatch.countDown();
try {
doneLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
assertEquals(numThread, resultList.stream().filter(item -> item.booleanValue()).count());
}
}
测试结果为:
aaa:null
java.lang.AssertionError:
Expected :100
Actual :98
表示只有一个线程执行这行代码:
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
所以初始化代码应该只由一个线程执行。
奇怪的是,执行这行代码后,某些线程的结果会是null
:
return stringEnumMap.get(enumClassName).get(symbol);
由于没有 NullPointerException,stringEnumMap.get(enumClassName)
必须 return innerMap
的引用。但是为什么调用innerMap
的get(symbol)
后会得到null
?
请帮忙,我整天都快疯了!
stringEnumMap
应该是 ConcurrentHashMap<String, Map<String,Enum>>
,并使用 computeIfAbsent
进行惰性初始化。
尝试移动
如果 (!stringEnumMap.containsKey(enumClassName))
和
return stringEnumMap.get(enumClassName).get(symbol);
进入同步块。
问题出在线路
List<Boolean> resultList = new LinkedList<>();
Note that this implementation is not synchronized.If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.If no such object exists, the list should be "wrapped" using the Collections.synchronizedListmethod. This is best done at creation time, to prevent accidental unsynchronized access to the list:
List list = Collections.synchronizedList(new LinkedList(...));
因为 LinkedList
不是线程安全的,在 add
操作期间可能会发生意外行为。
这导致 resultList
大小小于线程计数,因此预期计数小于结果计数。
要获得正确的结果,请按照建议添加 Collections.synchronizedList
。
虽然您的实施很好,但我建议您遵循 Matt Timmermans 的回答以获得更简单和可靠的解决方案。
ConcurrentMap
界面
正如其他人指出的那样,如果跨线程操作 Map
,您必须考虑并发性。
您可以自己处理并发访问。但没有必要。 Java 带有两个 Map
的实现,它们是为内部处理并发而构建的。这些实现实现了 ConcurrentMap
接口。
ConcurrentSkipListMap
ConcurrentHashMap
第一个按排序顺序维护键,实现NavigableMap
接口。
这是我编写的 table 来展示 Map
与 Java 11.
您可能会发现 ConcurrentMap
接口的其他第三方实现。