2017-12-01 5 views
2

Enums.ifPresent(Class, String) Anrufe Enums.getEnumConstants unter der Haube des Guava:Warum synchronisiert Guava Enums.ifPresent unter der Haube?

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

Warum es Block ein synchronisiertes braucht? Wäre das nicht eine schwere Leistung Strafe? Javas Enum.valueOf(Class, String) scheint keine zu brauchen. Wenn Synchronisation tatsächlich notwendig ist, warum ist sie dann so ineffizient? Man würde hoffen, wenn Enum im Cache vorhanden ist, kann es ohne Sperren abgerufen werden. Sperren Sie nur, wenn der Cache gefüllt werden muss.

Für Referenz: Maven Dependency

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

Edit: Durch das Sperren ich zu einer doppelten Kontrolle Sperre mich beziehe.

+0

'Wäre das nicht eine schwere Leistungseinbußen entstehen' Nicht unbedingt?. Insbesondere denke ich "schwer" ist eine flache Nr. Der Code im synchronisierten Block sieht so aus, als ob er auf ein statisches (globales) Cache-Objekt zugreift, daher wird wahrscheinlich die Synchronisation dafür benötigt. Ich nehme an, dass die 'valueOf()' Methode von Java auf ein * immutable * Objekt zugreift und daher keine Mutex oder andere Speicher Sichtbarkeitsoperationen benötigt. – markspace

+0

Es klingt, als ob Sie in Ihrem letzten Satz an eine doppelte Überprüfung denken. Bei modernen JVMs ist die unangefochtene Synchronisation sehr schnell. Ich denke, dass es hier verwendet wird, um eine Speicherbarriere zu schaffen, so dass, wenn "Konstanten" gefüllt sind, alle anderen Threads es garantiert sofort sehen werden, aber ich werde jemanden mit mehr Fachwissen antworten lassen. –

+0

Oh richtig, guter Fang. 'Wenn Enum im Cache vorhanden ist, kann es ohne Sperren abgerufen werden. Nein Nein Nein Nein Nein Nein. Sichtbarkeit (zwischen Threads) in Java erfordert eine Sperre sowohl von einem Schreiber als auch von einem Leser, und beide müssen dieselbe Sperre verwenden. Alles andere ist im Java-Speichermodell unterbrochen. Lesevorgänge müssen wie bei Schreibvorgängen gesperrt werden. Dies ist der Grund, warum Unveränderlichkeit eine große Sache ist, es erfordert NICHT, dass der Leser etwas Besonderes für die Sichtbarkeit des Speichers tut. Bitte lesen Sie Brian Goetz [Java Parallelität in der Praxis] (http://jcip.net/) für Details. – markspace

Antwort

1

Ich denke, der Grund ist einfach, dass enumConstantCache ist ein WeakHashMap, die nicht Thread-sicher ist.

Zwei Threads, die gleichzeitig in den Cache schreiben, könnten mit einer Endlosschleife oder ähnlich enden (zumindest passierte das mit HashMap, wie ich es vor Jahren versucht habe).

Ich denke, Sie könnten DCL verwenden, aber es kann nicht wert sein (wie in einem Kommentar angegeben).

Weiter, wenn Synchronisierung tatsächlich notwendig ist, warum ist es so ineffizient? Man würde hoffen, wenn Enum im Cache vorhanden ist, kann es ohne Sperren abgerufen werden. Sperren Sie nur, wenn der Cache gefüllt werden muss.

Dies kann zu schwierig werden. Für die Sichtbarkeit mit volatile benötigen Sie einen flüchtigen Lesevorgang, der mit einem flüchtigen Schreibvorgang gepaart ist. Sie können das flüchtige Lesen leicht erhalten, indem Sie enumConstantCache zu volatile statt final erklären. Das flüchtige Schreiben ist komplizierter. Etwas wie

enumConstantCache = enumConstantCache; 

kann funktionieren, aber ich bin mir nicht sicher.

10 Fäden, von denen jeder eines String-Werte zu Aufzählungen konvertieren und dann einige Aufgabe

Die Karte Zugang in der Regel führen ist viel schneller als alles, was Sie mit dem erhaltenen Wert zu tun, so dass ich denke, Sie würde viel mehr Threads benötigen, um ein Problem zu bekommen.


Im Gegensatz zu HashMap, muss die WeakHashMap einig Bereinigung durchzuführen (so genannten expungeStaleEntries). Diese Bereinigung wird auch in get (über getTable) durchgeführt. Also get ist eine modifizierende Operation und Sie wollen es nicht gleichzeitig ausführen.

Beachten Sie, dass das Lesen WeakHashMap ohne Synchronisation bedeutet, Mutation ohne Sperre durchzuführen und es ist einfach falsch und that's not only theory.

Sie würden eine eigene Version von WeakHashMap brauchen keine Mutationen in get Durchführung (was einfach ist) und einige vernünftigen Verhalten zu gewährleisten, wenn sie von einem anderen Thread während gelesen geschrieben (die können oder auch nicht möglich sein).

Ich denke, etwas wie SoftReference<ImmutableMap<String, Enum<?>> mit einigen Nachlade-Logik könnte gut funktionieren.

+0

Ich habe unten eine sehr grobe Benchmark als Antwort veröffentlicht. Es testet die Konvertierung von Enums mit 10 Threads gleichzeitig. –

1

Ich habe @maaartinus Antwort angenommen, aber wollte eine separate "Antwort" über die Umstände hinter der Frage und dem interessanten Kaninchenloch schreiben, zu dem es mich führt.

tl; dr - Verwendung von Java Enum.valueOf die thread safe ist und Synchronisierung nicht im Gegensatz zu Guava ist Enums.ifPresent. Auch in den meisten Fällen spielt es wahrscheinlich keine Rolle.

Lange Geschichte:

Ich bin auf einer Code-Basis arbeiten, die Java-Leichtgewichtler Threads nutzt Quasar Fibers. Um die Leistungsfähigkeit von Fibers zu nutzen, sollte der von ihnen ausgeführte Code hauptsächlich async und nicht blockierend sein, da Fasern zu Java/OS-Threads gemultiplext sind. Es wird sehr wichtig, dass einzelne Fasern den darunter liegenden Thread nicht "blockieren". Wenn der zugrundeliegende Thread blockiert ist, werden alle darauf laufenden Fibers blockiert und die Leistung wird erheblich beeinträchtigt. Guavas Enums.ifPresent ist einer dieser Blocker und ich bin mir sicher, dass es vermieden werden kann.

Zuerst begann ich Guava Enums.ifPresent zu verwenden, weil es null auf ungültige Enum-Werte zurückgibt. Im Gegensatz zu Javas Enum.valueOf, die IllegalArgumentException wirft (was meiner Meinung nach weniger vorzuziehen ist als ein Nullwert).

Hier ist eine grobe Benchmark verschiedene Verfahren zur Umwandlung in Aufzählungen Vergleich:

  1. Java Enum.valueOf mit IllegalArgumentException fangen null
  2. Guava des Enums.ifPresent
  3. Apache Commons Lang EnumUtils.getEnum
  4. Apache Commons Lang 3 zurück EnumUtils.getEnum
  5. Meine eigene benutzerdefinierte Immuta ble Karte Lookup

Hinweise:

  • Apache Commons Lang 3 verwendet Java Enum.valueOf unter der Haube und sind daher identisch
  • frühere Version von Apache Commons Lang verwendet eine sehr ähnliche WeakHashMap Lösung Guava aber tut verwende keine Synchronisation. Sie bevorzugen billige Lesevorgänge und teurere Schreibvorgänge (meine Reaktion auf den Kniestoß sagt, dass Guava es hätte tun sollen)
  • Javas Entscheidung, IllegalArgumentException zu werfen, ist wahrscheinlich mit geringen Kosten verbunden, wenn es um ungültige Enum-Werte geht. Das Werfen/Fangen von Ausnahmen ist nicht kostenlos.
  • Guava ist die einzige hier Methode, die Synchronisation

Benchmark-Konfiguration verwendet:

  • verwendet ein ExecutorService mit einem festen Thread-Pool von 10 Fäden
  • einreicht 100K Runnable Aufgaben Aufzählungen konvertieren
  • Jeder ausführbare Task konvertiert 100 Enums
  • Jede Methode zum Konvertieren von Enums konvertiert 10 Millionen Strings (100K x 100
  • )

Benchmark-Ergebnisse von einem Lauf:

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 

Diese Zahlen sollten mit einem schweren Körnchen Salz genommen werden und wird von anderen Faktoren ändern sich abhängig. Aber sie waren gut genug für mich, um mit der Lösung von Java für die Codebasis mit Fibers zu gehen.

Benchmark-Code:

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

Sie sollten JMH wirklich für das Benchmarking verwenden, da die Verwendung der Strategie das Ergebnis von der Reihenfolge abhängig macht (siehe zB meine Kommentare zu dieser [Antwort] (https://codereview.stackexchange.com/a/52344/14363) und manchmal falsch bei Bestellungen von Jede * Java * -Benchmark, die weniger als ein paar Sekunden dauert, ist wertlos.+++ Beachten Sie, dass das Lesen von 'WeakHashMap' ohne Synchronisation nicht auf billigen Lesevorgängen beruht - es ist völlig kaputt und [das ist nicht nur Theorie] (https://access.redhat.com/solutions/55161). – maaartinus

+0

Wenn Misses sehr selten sind, sieht die 'valueOf' Lösung ziemlich gut aus. Solange Sie sich nicht für das Entladen von Klassen interessieren, gibt es keinen Grund, keine 'ImmutableMap' zu verwenden. Falls du dies tust, könntest du eine lockfreie 'WeakHashMap' erstellen (wahrscheinlich zu viel Arbeit, aber lustig). – maaartinus

Verwandte Themen