为什么Guava Enums.ifPresent在引擎盖下同步使用?

时间:2017-12-01 19:37:19

标签: java enums synchronization guava

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

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.2-jre</version>
</dependency>

编辑:通过锁定我指的是双重检查锁。

2 个答案:

答案 0 :(得分:1)

我猜,原因很简单enumConstantCacheWeakHashMap,这不是线程安全的。

同时写入缓存的两个线程最终可能会出现无限循环或类似情况(至少在HashMap发生了这种情况,就像我多年前尝试过的那样)。

我想,你可以使用DCL,但它可能不值得(如评论中所述)。

  

如果确实需要同步,为什么这么低效呢?人们希望如果枚举存在于缓存中,则可以在不锁定的情况下检索它。只有在需要填充缓存时才锁定。

这可能太棘手了。要使用volatile进行可见性,您需要一个易失性读取与易失性写入配对。通过将enumConstantCache声明为volatile而不是final,您可以轻松获得易失性读取。易失性写入更棘手。像

这样的东西
enumConstantCache = enumConstantCache;

可能会奏效,但我对此并不确定。

  

10个线程,每个线程都必须将String值转换为Enums,然后执行一些任务

地图访问通常比使用获得的值更快地进行访问,因此我想,您需要更多线程来解决问题。

HashMap不同,WeakHashMap需要执行一些清理(称为expungeStaleEntries)。即使在get(通过getTable)也可以执行此清理。因此get是一项修改操作,您真的不想同时执行它。

请注意,在没有同步的情况下阅读WeakHashMap意味着在没有锁定的情况下执行变异,这是完全错误的that's not only theory

您需要WeakHashMap的{​​{1}}自己的版本在get中执行无变异(这很简单),并且在读取期间由不同的线程(可能会或可能会)保证一些明智的行为不可能)。

我想,像SoftReference<ImmutableMap<String, Enum<?>>这样的某些重新加载逻辑可以很好地工作。

答案 1 :(得分:1)

我已经接受了@maaartinus的答案,但是想写一个单独的“答案”,说明问题背后的情况以及它带给我的有趣的兔子洞。

  

tl; dr - 使用Java Enum.valueOfthread safe并且不像Guava的Enums.ifPresent那样同步。同样在大多数情况下,它可能无关紧要。

长篇故事:

我正在开发一个利用轻量级java线程Quasar Fibers的代码库。为了利用Fibers的强大功能,它们运行的​​代码应该主要是异步和非阻塞,因为Fibers被多路复用到Java / OS线程。单个Fibers不会“阻塞”底层线程变得非常重要。如果底层线程被阻塞,它将阻止在其上运行的所有光纤并且性能显着下降。番石榴Enums.ifPresent是其中一个阻挡者,我确信它可以避免。

最初,我开始使用Guava的Enums.ifPresent,因为它会在无效的枚举值上返回null。与引用Enum.valueOf的Java IllegalArgumentException不同(根据我的口味,它比空值更不可取)。

以下是比较各种转换为枚举的方法的粗略基准:

  1. Java Enum.valueOf抓住IllegalArgumentException返回null
  2. Guava的Enums.ifPresent
  3. Apache Commons Lang EnumUtils.getEnum
  4. Apache Commons Lang 3 EnumUtils.getEnum
  5. 我自己的自定义不可变地图查找
  6. 注意:

    • Apache Common Lang 3使用Java的Enum.valueOf,因此是相同的
    • 早期版本的Apache Common Lang使用与Guava非常相似的WeakHashMap解决方案,但不使用同步。他们喜欢便宜的阅读和更昂贵的写作(我的膝盖反射说反应是Guava应该如何做到的)
    • 在处理无效的枚举值时,Java决定抛出IllegalArgumentException可能会产生相关的小成本。投掷/捕获异常不是免费的。
    • 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
    

    这些数字应该含有大量的盐,并会根据其他因素而改变。但它们足以让我最终使用Fibers来使用Java的代码库解决方案。

    基准代码:

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