2017-05-28 3 views
8

Javadoc von ConcurrentHashMap#computeIfAbsent sagenFolgen andere wichtige Aktualisierung (en) in ConcurrentHashMap # computeIfAbsent

Die Berechnung sollte kurz und einfach, und dürfen keine andere Zuordnungen dieser Karte zu Update versuchen.

Aber von dem, was ich sehe, mit remove() und clear() Methoden innerhalb mappingFunction funktioniert gut. Zum Beispiel diese

Key element = elements.computeIfAbsent(key, e -> { 
    if (usages.size() == maxSize) { 
     elements.remove(oldest); 
    } 
    return loader.load(key); 
}); 

Welche schlechten Folgen entfernen() -Methode innerhalb mappingFunction könnte?

+0

Eine solche Spezifikation sollte sehr sorgfältig gelesen werden. Die Wörter "darf nicht versuchen" zeigt an, dass Sie wahrscheinlich irgendwo etwas brechen, wenn Sie der Regel nicht folgen. Aber im Vergleich zu Andy interessiert mich immer sehr, warum solche Restriktionen eingeführt werden. –

Antwort

4

Die javadoc erklärt eindeutig die Ursache:

Einige versuchten Update-Operationen an dieser Stelle durch andere Threads sein kann blockiert, während Berechnungen laufen, so sollte die Berechnung kurz und einfach sein und darf nicht versuchen, andere Zuordnungen von dieser Karte zu aktualisieren.

Sie haben nicht zu vergessen, dass ConcurrentHashMap ausgelegt ist, einen Thread sichere Karte, einen Weg zu schaffen, die Verwendung, ohne das Sperren, wie es der Fall für ältere Threadsicherheit Map-Klassen als HashTable ist.
Wenn eine Änderung auf der Karte auftritt, wird nur das betreffende Mapping und nicht die gesamte Map gesperrt.

ConcurrentHashMap ist eine Hash-Tabelle für Updates vollständige Parallelität von Wiedergewinnungen und hohen erwarteten concurrency unterstützen.

computeIfAbsent() ist eine neue Methode in Java hinzugefügt 8.
Wenn das heißt, wenn im Körper computeIfAbsent() die bereits verriegelt die Abbildung der Schlüssel übergeben der Methode, Sie sperren einen anderen Schlüssel schlecht verwendet, Sie Geben Sie einen Pfad ein, in dem Sie den Zweck der ConcurrentHashMap ablehnen können, da Sie schließlich zwei Mappings sperren.
Stellen Sie sich das Problem vor, wenn Sie mehr Mapping innerhalb computeIfAbsent() sperren und dass die Methode überhaupt nicht kurz ist. Der Zugriff auf die Karte auf Nebenläufigkeit wird langsam.

So betont das Javadoc computeIfAbsent() auf dieses potenzielle Problem durch das Erinnern der Prinzipien von ConcurrentHashMap: halten Sie es einfach und schnell.


Hier ist ein Beispielcode, der das Problem veranschaulicht.
Angenommen, wir haben eine Instanz von ConcurrentHashMap<Integer, String>.

Wir werden zwei Threads beginnen, die sie verwenden:

  • Der erste Thread: thread1, die computeIfAbsent() mit dem Schlüssel 1
  • des zweiten Thread aufruft: thread2 die computeIfAbsent() mit dem Schlüssel 2
rufe

führt eine schnell genug Aufgabe, aber es folgt nicht beraten von die computeIfAbsent() javadoc: es aktualisiert den Schlüssel 2 in computeIfAbsent(), das ist eine andere Zuordnung von denen im aktuellen Kontext der Methode verwendet (das ist Schlüssel 1).

thread2 führt eine ausreichend lange Aufgabe aus. Es ruft computeIfAbsent() mit dem Schlüssel 2 auf, indem es den Javadoc-Anweisungen folgt: es aktualisiert kein anderes Mapping in der Implementierung von es.
Um die lange Aufgabe zu simulieren, können wir die Thread.sleep() Methode mit 5000 als Parameter verwenden.

Für diese spezielle Situation, wenn thread2 beginnt vor thread1, wird der Aufruf von map.put(2, someValue); in thread1 während thread2 blockiert werden nicht von computeIfAbsent() zurückgegeben, die die Zuordnung des Schlüssels 2 sperrt.

Schließlich erhalten wir ein ConcurrentHashMap Beispiel blockiert die Abbildung der Taste 2 während 5 Sekunden, während computeIfAbsent() mit der Abbildung des Schlüssel 1 aufgerufen wird.
Es ist irreführend, nicht wirksam und es geht gegen die ConcurrentHashMap Absicht und der computeIfAbsent() Beschreibung, die die Absicht für die aktuelle Taste, um den Wert der Berechnung:

wenn der angegebene Schlüssel nicht bereits mit einem Wert zugeordnet ist, versucht seinen Wert mit der gegebenen Abbildungsfunktion zu berechnen, und gibt sie in diese Karte sofern nicht null

Der Beispielcode:

import java.util.concurrent.ConcurrentHashMap; 

public class BlockingCallOfComputeIfAbsentWithConcurrentHashMap { 

    public static void main(String[] args) throws InterruptedException { 
    ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(); 

    Thread thread1 = new Thread() { 
     @Override 
     public void run() { 
      map.computeIfAbsent(1, e -> { 
       String valueForKey2 = map.get(2); 
       System.out.println("thread1 : get() returns with value for key 2 = " + valueForKey2); 
       String oldValueForKey2 = map.put(2, "newValue"); 
       System.out.println("thread1 : after put() returns, previous value for key 2 = " + oldValueForKey2); 
       return map.get(2); 
      }); 
     } 
    }; 

    Thread thread2 = new Thread() { 
     @Override 
     public void run() { 
      map.computeIfAbsent(2, e -> { 
      try { 
       Thread.sleep(5000); 
      } catch (Exception e1) { 
       e1.printStackTrace(); 
      } 
      String value = "valueSetByThread2"; 
      System.out.println("thread2 : computeIfAbsent() returns with value for key 2 = " + value); 
      return value; 
      }); 
     } 
    }; 

    thread2.start(); 
    Thread.sleep(1000); 
    thread1.start(); 
    } 
} 

Als Ausgang wir immer bekommen:

thread1: get() gibt mit dem Wert für die Taste 2 = null

thread2: computeIfAbsent() gibt mit dem Wert für die Taste 2 = valueSetByThread2

thread1: nach put() zurückkehrt, vorherigen Wert für Taste 2 = valueSetByThread2

Dies ist writt en schnell wie auf den ConcurrentHashMap liest nicht blockieren:

thread1: get() für die Taste 2 = null

mit Wert zurückgibt

aber:

thread1: nach put () zurück, vorheriger Wert für Taste 2 = valueSetByThread2

wird nur ausgegeben, wenn thread2 zurückgegeben wird von computeIfAbsent().

5

Hier ist ein Beispiel für eine schlechte Folge:

ConcurrentHashMap<Integer,String> cmap = new ConcurrentHashMap<>(); 
cmap.computeIfAbsent (1, e-> {cmap.remove (1); return "x";}); 

Dieser Code ein Deadlock verursacht.

2

Solche Ratschläge sind ein bisschen wie der Rat, nicht mitten auf einer Straße zu gehen. Sie können es tun, und Sie werden vielleicht nicht von einem Auto angefahren; Sie können auch aus dem Weg gehen, wenn Sie das Auto kommen sehen.

Aber Sie wären sicherer gewesen, wenn Sie nur auf dem Bürgersteig (Bürgersteig) geblieben wären.

Wenn ein API-Dokument Ihnen sagt, dass Sie etwas nicht tun sollen, steht Ihnen natürlich nichts im Wege. Und Sie könnten versuchen, es zu tun, und feststellen, dass es keine negativen Konsequenzen gibt, zumindest in den begrenzten Umständen, die Sie testen. Sie können sogar graben, um die genauen Gründe herauszufinden, warum der Rat da ist; Sie können den Quellcode überprüfen und nachweisen, dass er in Ihrem Anwendungsfall sicher ist.

Den Implementierern der API steht es jedoch frei, die Implementierung innerhalb der durch die API-Dokumentation beschriebenen Einschränkungen des Vertrags zu ändern. Sie können eine Änderung vornehmen, die den Code morgen nicht mehr unterstützt, da sie nicht verpflichtet sind, Verhalten zu bewahren, das ausdrücklich vor der Verwendung gewarnt wird.

Also, um Ihre Frage zu beantworten, was die schlimmen Folgen sein könnten: buchstäblich alles (na ja, alles, was normalerweise fertig ist oder eine RuntimeException wirft); und Sie würden nicht unbedingt die gleichen Konsequenzen im Laufe der Zeit oder bei verschiedenen JVMs beobachten.

Bleiben Sie auf dem Bürgersteig: tun Sie nicht, was die Dokumentation Ihnen nicht sagt.

Verwandte Themen