为什么 Guava Enums.ifPresent 在后台使用同步?
Why does Guava Enums.ifPresent use synchronized under the hood?
Guava 的 Enums.ifPresent(Class, String)
底层调用 Enums.getEnumConstants
:
@GwtIncompatible // java.lang.ref.WeakReference
static <T extends Enum<T>> Map<String, WeakReference<? extends Enum<?>>> getEnumConstants(
Class<T> enumClass) {
synchronized (enumConstantCache) {
Map<String, WeakReference<? extends Enum<?>>> constants = enumConstantCache.get(enumClass);
if (constants == null) {
constants = populateCache(enumClass);
}
return constants;
}
}
为什么需要同步块?这不会导致 严重 性能损失吗? Java 的 Enum.valueOf(Class, String)
似乎不需要。更进一步,如果确实需要同步,为什么这样做效率如此低下?人们希望如果枚举存在于缓存中,则可以在不锁定的情况下检索它。仅在需要填充缓存时锁定。
供参考:Maven 依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.2-jre</version>
</dependency>
编辑:通过锁定,我指的是双重检查锁。
我猜,原因很简单,enumConstantCache
是一个WeakHashMap
,它不是线程安全的。
同时写入缓存的两个线程可能会以无限循环或类似的方式结束(至少我几年前尝试过 HashMap
时发生过这种情况)。
我想,您可以使用 DCL,但它可能不值得(如评论中所述)。
Further on if synchronization is indeed necessary, why do it so inefficiently? One would hope if enum is present in cache, it can be retrieved without locking. Only lock if cache needs to be populated.
这可能太棘手了。对于使用 volatile
的可见性,您需要将易失性读取与易失性写入配对。您可以通过将 enumConstantCache
声明为 volatile
而不是 final
来轻松读取 volatile。易失性写入更棘手。像
enumConstantCache = enumConstantCache;
可能有用,但我不确定。
10 threads, each one having to convert String values to Enums and then perform some task
Map 访问通常比您对获取的值所做的任何操作都快,所以我想,您需要更多的线程才能解决问题。
与 HashMap
不同,WeakHashMap
需要执行一些清理(称为 expungeStaleEntries
)。即使在 get
中(通过 getTable
)也会执行此清理。所以 get
是一个修改操作,你真的不想并发执行它。
请注意,在没有同步的情况下读取 WeakHashMap
意味着在没有锁定的情况下执行变异,这是完全错误的,that's not only theory。
你需要一个自己的 WeakHashMap
版本,在 get
中不执行任何突变(这很简单),并保证在不同线程读取期间写入时有一些正常的行为(这可能会或可能不可能)。
我想,像 SoftReference<ImmutableMap<String, Enum<?>>
这样带有一些重新加载逻辑的东西可以很好地工作。
我已经接受了@maaartinus 的回答,但想单独写一篇 "answer" 关于问题背后的情况以及它把我带到的有趣的兔子洞。
tl;dr - Use Java's Enum.valueOf
which is thread safe and does not sync unlike Guava's Enums.ifPresent
. Also in majority of cases it probably doesn't matter.
长话短说:
我正在开发一个使用轻量级 java 线程 Quasar Fibers 的代码库。为了利用纤程的力量,它们 运行 的代码应该主要是异步和非阻塞的,因为纤程被多路复用到 Java/OS 线程。个别纤程不 "block" 底层线程变得非常重要。如果底层线程被阻塞,它将阻塞其上的所有 Fibers 运行ning 并且性能会大大降低。 Guava 的 Enums.ifPresent
就是其中之一,我确信它是可以避免的。
最初,我开始使用 Guava 的 Enums.ifPresent
,因为它 returns null
用于无效的枚举值。与 Java 的 Enum.valueOf
不同,它抛出 IllegalArgumentException
(根据我的口味,这比空值更不受欢迎)。
这里是比较各种转换为枚举的方法的粗略基准:
- Java 的
Enum.valueOf
与捕获 IllegalArgumentException
到 return null
- 番石榴的
Enums.ifPresent
- Apache Commons Lang
EnumUtils.getEnum
- Apache Commons Lang 3
EnumUtils.getEnum
- 我自己的自定义不可变地图查找
备注:
- Apache Common Lang 3 在底层使用 Java 的
Enum.valueOf
,因此是相同的
- 早期版本的 Apache Common Lang 使用与 Guava 非常相似的
WeakHashMap
解决方案,但不使用同步。他们喜欢便宜的读取和更昂贵的写入(我的膝跳反应说这就是 Guava 应该做的)
在处理无效的枚举值时,- Java 抛出
IllegalArgumentException
的决定可能会带来很小的成本。 Throwing/catching 例外不是免费的。
- Guava 是这里唯一使用同步的方法
基准设置:
- 使用具有 10 个线程的固定线程池的
ExecutorService
- 提交 100K Runnable 任务来转换枚举
- 每个 Runnable 任务转换 100 个枚举
- 每个转换枚举的方法将转换 1000 万个字符串 (100K x 100)
来自 运行 的基准测试结果:
Convert valid enum string value:
JAVA -> 222 ms
GUAVA -> 964 ms
APACHE_COMMONS_LANG -> 138 ms
APACHE_COMMONS_LANG3 -> 149 ms
MY_OWN_CUSTOM_LOOKUP -> 160 ms
Try to convert INVALID enum string value:
JAVA -> 6009 ms
GUAVA -> 734 ms
APACHE_COMMONS_LANG -> 65 ms
APACHE_COMMONS_LANG3 -> 5558 ms
MY_OWN_CUSTOM_LOOKUP -> 92 ms
这些数字应持保留态度,并且会根据其他因素而变化。但它们足以让我决定采用 Java 的使用 Fibers 的代码库解决方案。
基准代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Enums;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
public class BenchmarkEnumValueOf {
enum Strategy {
JAVA,
GUAVA,
APACHE_COMMONS_LANG,
APACHE_COMMONS_LANG3,
MY_OWN_CUSTOM_LOOKUP;
private final static ImmutableMap<String, Strategy> lookup;
static {
Builder<String, Strategy> immutableMapBuilder = ImmutableMap.builder();
for (Strategy strategy : Strategy.values()) {
immutableMapBuilder.put(strategy.name(), strategy);
}
lookup = immutableMapBuilder.build();
}
static Strategy toEnum(String name) {
return name != null ? lookup.get(name) : null;
}
}
public static void main(String[] args) {
final int BENCHMARKS_TO_RUN = 1;
System.out.println("Convert valid enum string value:");
for (int i = 0; i < BENCHMARKS_TO_RUN; i++) {
for (Strategy strategy : Strategy.values()) {
runBenchmark(strategy, "JAVA", 100_000);
}
}
System.out.println("\nTry to convert INVALID enum string value:");
for (int i = 0; i < BENCHMARKS_TO_RUN; i++) {
for (Strategy strategy : Strategy.values()) {
runBenchmark(strategy, "INVALID_ENUM", 100_000);
}
}
}
static void runBenchmark(Strategy strategy, String enumStringValue, int iterations) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
long timeStart = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
executorService.submit(new EnumValueOfRunnable(strategy, enumStringValue));
}
executorService.shutdown();
try {
executorService.awaitTermination(1000, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long timeDuration = System.currentTimeMillis() - timeStart;
System.out.println("\t" + strategy.name() + " -> " + timeDuration + " ms");
}
static class EnumValueOfRunnable implements Runnable {
Strategy strategy;
String enumStringValue;
EnumValueOfRunnable(Strategy strategy, String enumStringValue) {
this.strategy = strategy;
this.enumStringValue = enumStringValue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
switch (strategy) {
case JAVA:
try {
Enum.valueOf(Strategy.class, enumStringValue);
} catch (IllegalArgumentException e) {}
break;
case GUAVA:
Enums.getIfPresent(Strategy.class, enumStringValue);
break;
case APACHE_COMMONS_LANG:
org.apache.commons.lang.enums.EnumUtils.getEnum(Strategy.class, enumStringValue);
break;
case APACHE_COMMONS_LANG3:
org.apache.commons.lang3.EnumUtils.getEnum(Strategy.class, enumStringValue);
break;
case MY_OWN_CUSTOM_LOOKUP:
Strategy.toEnum(enumStringValue);
break;
}
}
}
}
}
Guava 的 Enums.ifPresent(Class, String)
底层调用 Enums.getEnumConstants
:
@GwtIncompatible // java.lang.ref.WeakReference
static <T extends Enum<T>> Map<String, WeakReference<? extends Enum<?>>> getEnumConstants(
Class<T> enumClass) {
synchronized (enumConstantCache) {
Map<String, WeakReference<? extends Enum<?>>> constants = enumConstantCache.get(enumClass);
if (constants == null) {
constants = populateCache(enumClass);
}
return constants;
}
}
为什么需要同步块?这不会导致 严重 性能损失吗? Java 的 Enum.valueOf(Class, String)
似乎不需要。更进一步,如果确实需要同步,为什么这样做效率如此低下?人们希望如果枚举存在于缓存中,则可以在不锁定的情况下检索它。仅在需要填充缓存时锁定。
供参考:Maven 依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.2-jre</version>
</dependency>
编辑:通过锁定,我指的是双重检查锁。
我猜,原因很简单,enumConstantCache
是一个WeakHashMap
,它不是线程安全的。
同时写入缓存的两个线程可能会以无限循环或类似的方式结束(至少我几年前尝试过 HashMap
时发生过这种情况)。
我想,您可以使用 DCL,但它可能不值得(如评论中所述)。
Further on if synchronization is indeed necessary, why do it so inefficiently? One would hope if enum is present in cache, it can be retrieved without locking. Only lock if cache needs to be populated.
这可能太棘手了。对于使用 volatile
的可见性,您需要将易失性读取与易失性写入配对。您可以通过将 enumConstantCache
声明为 volatile
而不是 final
来轻松读取 volatile。易失性写入更棘手。像
enumConstantCache = enumConstantCache;
可能有用,但我不确定。
10 threads, each one having to convert String values to Enums and then perform some task
Map 访问通常比您对获取的值所做的任何操作都快,所以我想,您需要更多的线程才能解决问题。
与 HashMap
不同,WeakHashMap
需要执行一些清理(称为 expungeStaleEntries
)。即使在 get
中(通过 getTable
)也会执行此清理。所以 get
是一个修改操作,你真的不想并发执行它。
请注意,在没有同步的情况下读取 WeakHashMap
意味着在没有锁定的情况下执行变异,这是完全错误的,that's not only theory。
你需要一个自己的 WeakHashMap
版本,在 get
中不执行任何突变(这很简单),并保证在不同线程读取期间写入时有一些正常的行为(这可能会或可能不可能)。
我想,像 SoftReference<ImmutableMap<String, Enum<?>>
这样带有一些重新加载逻辑的东西可以很好地工作。
我已经接受了@maaartinus 的回答,但想单独写一篇 "answer" 关于问题背后的情况以及它把我带到的有趣的兔子洞。
tl;dr - Use Java's
Enum.valueOf
which is thread safe and does not sync unlike Guava'sEnums.ifPresent
. Also in majority of cases it probably doesn't matter.
长话短说:
我正在开发一个使用轻量级 java 线程 Quasar Fibers 的代码库。为了利用纤程的力量,它们 运行 的代码应该主要是异步和非阻塞的,因为纤程被多路复用到 Java/OS 线程。个别纤程不 "block" 底层线程变得非常重要。如果底层线程被阻塞,它将阻塞其上的所有 Fibers 运行ning 并且性能会大大降低。 Guava 的 Enums.ifPresent
就是其中之一,我确信它是可以避免的。
最初,我开始使用 Guava 的 Enums.ifPresent
,因为它 returns null
用于无效的枚举值。与 Java 的 Enum.valueOf
不同,它抛出 IllegalArgumentException
(根据我的口味,这比空值更不受欢迎)。
这里是比较各种转换为枚举的方法的粗略基准:
- Java 的
Enum.valueOf
与捕获IllegalArgumentException
到 returnnull
- 番石榴的
Enums.ifPresent
- Apache Commons Lang
EnumUtils.getEnum
- Apache Commons Lang 3
EnumUtils.getEnum
- 我自己的自定义不可变地图查找
备注:
- Apache Common Lang 3 在底层使用 Java 的
Enum.valueOf
,因此是相同的 - 早期版本的 Apache Common Lang 使用与 Guava 非常相似的
WeakHashMap
解决方案,但不使用同步。他们喜欢便宜的读取和更昂贵的写入(我的膝跳反应说这就是 Guava 应该做的)
在处理无效的枚举值时, - Java 抛出
IllegalArgumentException
的决定可能会带来很小的成本。 Throwing/catching 例外不是免费的。 - Guava 是这里唯一使用同步的方法
基准设置:
- 使用具有 10 个线程的固定线程池的
ExecutorService
- 提交 100K Runnable 任务来转换枚举
- 每个 Runnable 任务转换 100 个枚举
- 每个转换枚举的方法将转换 1000 万个字符串 (100K x 100)
来自 运行 的基准测试结果:
Convert valid enum string value:
JAVA -> 222 ms
GUAVA -> 964 ms
APACHE_COMMONS_LANG -> 138 ms
APACHE_COMMONS_LANG3 -> 149 ms
MY_OWN_CUSTOM_LOOKUP -> 160 ms
Try to convert INVALID enum string value:
JAVA -> 6009 ms
GUAVA -> 734 ms
APACHE_COMMONS_LANG -> 65 ms
APACHE_COMMONS_LANG3 -> 5558 ms
MY_OWN_CUSTOM_LOOKUP -> 92 ms
这些数字应持保留态度,并且会根据其他因素而变化。但它们足以让我决定采用 Java 的使用 Fibers 的代码库解决方案。
基准代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Enums;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
public class BenchmarkEnumValueOf {
enum Strategy {
JAVA,
GUAVA,
APACHE_COMMONS_LANG,
APACHE_COMMONS_LANG3,
MY_OWN_CUSTOM_LOOKUP;
private final static ImmutableMap<String, Strategy> lookup;
static {
Builder<String, Strategy> immutableMapBuilder = ImmutableMap.builder();
for (Strategy strategy : Strategy.values()) {
immutableMapBuilder.put(strategy.name(), strategy);
}
lookup = immutableMapBuilder.build();
}
static Strategy toEnum(String name) {
return name != null ? lookup.get(name) : null;
}
}
public static void main(String[] args) {
final int BENCHMARKS_TO_RUN = 1;
System.out.println("Convert valid enum string value:");
for (int i = 0; i < BENCHMARKS_TO_RUN; i++) {
for (Strategy strategy : Strategy.values()) {
runBenchmark(strategy, "JAVA", 100_000);
}
}
System.out.println("\nTry to convert INVALID enum string value:");
for (int i = 0; i < BENCHMARKS_TO_RUN; i++) {
for (Strategy strategy : Strategy.values()) {
runBenchmark(strategy, "INVALID_ENUM", 100_000);
}
}
}
static void runBenchmark(Strategy strategy, String enumStringValue, int iterations) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
long timeStart = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
executorService.submit(new EnumValueOfRunnable(strategy, enumStringValue));
}
executorService.shutdown();
try {
executorService.awaitTermination(1000, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long timeDuration = System.currentTimeMillis() - timeStart;
System.out.println("\t" + strategy.name() + " -> " + timeDuration + " ms");
}
static class EnumValueOfRunnable implements Runnable {
Strategy strategy;
String enumStringValue;
EnumValueOfRunnable(Strategy strategy, String enumStringValue) {
this.strategy = strategy;
this.enumStringValue = enumStringValue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
switch (strategy) {
case JAVA:
try {
Enum.valueOf(Strategy.class, enumStringValue);
} catch (IllegalArgumentException e) {}
break;
case GUAVA:
Enums.getIfPresent(Strategy.class, enumStringValue);
break;
case APACHE_COMMONS_LANG:
org.apache.commons.lang.enums.EnumUtils.getEnum(Strategy.class, enumStringValue);
break;
case APACHE_COMMONS_LANG3:
org.apache.commons.lang3.EnumUtils.getEnum(Strategy.class, enumStringValue);
break;
case MY_OWN_CUSTOM_LOOKUP:
Strategy.toEnum(enumStringValue);
break;
}
}
}
}
}