2017-12-01 22 views
2

グアバのEnums.ifPresent(Class, String)通話Enums.getEnumConstantsGuava Enums.ifPresentをフードの下で同期させるのはなぜですか?ボンネットの下に

@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> 

編集:ロックすることにより、私はダブルチェックロックを参照しています。

+0

は 'それは重いパフォーマンスの低下を招くと思いません'必ずしもそうではありません?。特に私は、 "重い"はフラットなノーだと思う。同期ブロック内のコードは、静的(グローバル)キャッシュオブジェクトにアクセスするように見えます。そのため、同期が必要になる可能性があります。私は、Javaの 'valueOf()'メソッドが* immutable *オブジェクトにアクセスするため、ミューテックスやその他のメモリの可視性操作を必要としないと考えています。 – markspace

+0

あなたの最後の文章で二重チェックロックを考えているようです。現代のJVMでは、無条件の同期が非常に高速です。私はそれがメモリバリアを作成するためにここで使用されていると思います。そのため、 '定数 'が読み込まれると、他のすべてのスレッドがすぐにそれを見ることが保証されます。 –

+0

ああ、良いキャッチ。 'enumがキャッシュに存在する場合、ロックせずに取得できます。いいえいいえいいえいいえ。 Javaの可視性(スレッド間)は、ライターとリーダーの両方によるロックが必要であり、どちらも同じロックを使用する必要があります。 Javaメモリモデルではそれ以外のものは壊れています。読み込みは、書き込みと同様にロックが必要です。これが不変性が大きな問題である理由です。読者がメモリの可視性に特別なことをする必要はありません。詳細については、Brian Goetzの[Java Concurrency in Practice](http://jcip.net/)を参照してください。 – markspace

答えて

1

私は、理由は単純にenumConstantCacheWeakHashMapであり、スレッドセーフではないと考えています。

同時にキャッシュに書き込む2つのスレッドは、無限ループまたは同様のものに終わる可能性があります(少なくとも、これまで数年前に試したようにHashMapで起こりました)。

DCLを使用することもできますが、(コメントに記載されているように)価値があるかもしれません。

さらに、同期が本当に必要な場合は、非効率的な理由は何ですか?列挙型がキャッシュに存在すれば、ロックせずに取り出すことができます。キャッシュに値を設定する必要がある場合にのみロックします。

これはあまりにも扱いにくいことがあります。 volatileを使用して可視性を確認するには、揮発性の読み取りと揮発性の書き込みを組み合わせる必要があります。あなたはenumConstantCachefinalの代わりにvolatileと宣言することによって、揮発性の読みを簡単に得ることができます。揮発性の書き込みはよりトリッキーです。何かのように

が動作するかもしれませんが、私はそれについては分かりません。

10スレッド、列挙型に文字列値を変換して、地図へのアクセスは通常、あなたが取得した値で行うものよりも道も高速ですので、私は推測するいくつかのタスク

を実行する必要が各1、あなた問題を起こすためにもっと多くのスレッドが必要です。 HashMapとは異なり


WeakHashMapは(expungeStaleEntriesと呼ばれる)いくつかのクリーンアップを実行する必要があります。このクリーンアップはgetgetTable経由)でも実行されます。したがってgetは変更操作であり、実際には同時に実行する必要はありません。

WeakHashMapを同期させずに読むと、ロックせずに突然変異を実行することを意味し、それは間違っているとよくあります。です。

getに変異を起こさず、別のスレッドによる読み込み時に書かれたときに正常な動作を保証する必要があります(これは可能かもしれません)。

再ロードロジックを持つSoftReference<ImmutableMap<String, Enum<?>>のようなものがうまくいくと思います。

+0

私は以下の答えとして非常に原油のベンチマークを掲示しました。 10個のスレッドを同時に列挙して変換をテストします。 –

1

私は@maaartinusの回答を受け入れましたが、質問の背後にある状況と興味深いウサギの穴について、別の「回答」を書いてみたかったのです。

TL; DR - thread safeで、グァバのEnums.ifPresentとは違って同期しません使用するJavaのEnum.valueOf。大部分の場合、おそらくそれは問題ではありません。

長い物語:

私は軽量のJavaスレッドQuasar Fibersを利用してコードベースに取り組んでいます。 Fibersのパワーを活用するために、コードはJava/OSスレッドに多重化されるため、実行されるコードは主に非同期で非ブロッキングにする必要があります。個々のファイバーが基礎となるスレッドを「ブロック」しないことが非常に重要になります。基底のスレッドがブロックされていると、実行されているすべてのファイバーがブロックされ、パフォーマンスが大幅に低下します。グアバのEnums.ifPresentは、これらのブロッカーの1つであり、私はそれが避けられ得ると確信しています。

最初は、無効な列挙値にnullを返すため、GuavaのEnums.ifPresentを使用しました。 JavaのEnum.valueOfと異なり、IllegalArgumentException(これは私の好みではnull値よりも優先度が低い)です。ここ

列挙型に変換する種々の方法を比較粗ベンチマークである:

  1. JavaのEnum.valueOfnull
  2. グアバのEnums.ifPresent
  3. アパッチ・コモンズラングEnumUtils.getEnum
  4. アパッチ・コモンズラング3を返すようにIllegalArgumentExceptionを引くとEnumUtils.getEnum
  5. My Own Custom Immuta BLEマップ検索

注:

  • Apacheの共通ラング3は、JavaのEnum.valueOfボンネットの下に使用していますので、Apacheの共通ラングの
  • 以前のバージョンと同じですグアバに非常によく似たWeakHashMapソリューションを使用していますが、い同期を使用しないでください。彼らは安価な読み込みとより高価な書き込みを好む(私の膝の反動反応はグアバがそれをしたはずだと言っている)
  • 無効な列挙値を扱うときにIllegalArgumentExceptionをスローするJavaの決定には、投げ捨て/例外のキャッチは無料ではありません。
  • グアバは同期

ベンチマークセットアップ使用していますここでの唯一の方法である:10件のスレッド

  • の固定されたスレッドプールは、列挙型
  • を変換するために、100KのRunnableタスクを送信して

    • ExecutorServiceを使用しています
    • 各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; 
           } 
          } 
         } 
        } 
    
    } 
    
    +0

    ベンチマークにはJMHを使用するべきです。戦略を使用すると結果は順序に依存します(この[回答](https://codereview.stackexchange.com/a/52344/14363)に関する私のコメントを参照してください。 * Java *のベンチマークは数秒もかからずに無駄です。+++同期化せずに 'WeakHashMap'を読むことは安価な読み込みではなく、完全に壊れてしまっています(理論だけではありません)(https://access.redhat.com/solutions/55161)。 – maaartinus

    +0

    ミスが非常にまれである場合、 'valueOf'ソリューションはかなり良く見えます。クラスのアンロードを気にしない限り、 'ImmutableMap'を使わない理由はありません。場合には、ロックフリーのWeakHashMap(おそらくあまりにも多くの仕事が面白い)を構築することができます。 – maaartinus

    関連する問題