webentwicklung-frage-antwort-db.com.de

Synchronisieren von String-Objekten in Java

Ich habe eine Web-App, an der ich gerade einige Last-/Leistungstests durchführe, insbesondere bei einer Funktion, bei der einige hundert Benutzer davon ausgehen, dass sie auf dieselbe Seite zugreifen und etwa alle 10 Sekunden auf dieser Seite eine Aktualisierung durchführen. Ein Verbesserungspotenzial, das wir mit dieser Funktion feststellen konnten, war das Zwischenspeichern der Antworten des Webservice für einige Zeit, da sich die Daten nicht ändern.

Nachdem ich dieses grundlegende Zwischenspeichern implementiert hatte, stellte ich in einigen weiteren Tests fest, dass ich nicht in Betracht zog, wie gleichzeitige Threads gleichzeitig auf den Cache zugreifen können. Ich fand heraus, dass innerhalb von ~ 100ms etwa 50 Threads versuchten, das Objekt aus dem Cache abzurufen, dass es abgelaufen war, den Web-Service zum Abrufen der Daten und dann das Objekt in den Cache zurücklegten.

Der ursprüngliche Code sah ungefähr so ​​aus:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  final String key = "Data-" + email;
  SomeData[] data = (SomeData[]) StaticCache.get(key);

  if (data == null) {
      data = service.getSomeDataForEmail(email);

      StaticCache.set(key, data, CACHE_TIME);
  }
  else {
      logger.debug("getSomeDataForEmail: using cached object");
  }

  return data;
}

Um sicherzustellen, dass nur ein Thread den Web-Service aufruft, als das Objekt bei key abgelaufen ist, dachte ich, ich müsste die Cache-Get/Set-Operation synchronisieren, und es schien, als wäre der Cache-Schlüssel ein guter Kandidat für ein Objekt zur Synchronisierung auf (Auf diese Weise werden Aufrufe an diese Methode für E-Mail [email protected] nicht durch Methodenaufrufe an [email protected] blockiert).

Ich habe die Methode folgendermaßen aktualisiert:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

Ich habe auch Protokollzeilen für Dinge wie "vor dem Synchronisationsblock", "innerhalb des Synchronisationsblocks", "kurz vor dem Verlassen des Synchronisationsblocks" und "nach dem Synchronisationsblock" hinzugefügt, sodass ich feststellen konnte, ob ich die get/set-Operation effektiv synchronisierte.

Es scheint jedoch nicht, dass dies funktioniert hat. Meine Testprotokolle wurden wie folgt ausgegeben:

(Protokollausgabe ist 'Threadname' 'Logger-Name' 'Nachricht')
http-80-Processor253 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor253 jsp.view-page - getSomeDataForEmail: innerhalb des Synchronisationsblocks
http-80-Processor253 cache.StaticCache - get: object at key [[email protected]] ist abgelaufen
http-80-Processor253 cache.StaticCache - get: key [[email protected]] Rückgabewert [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor263 jsp.view-page - getSomeDataForEmail: innerhalb des Synchronisationsblocks
http-80-Processor263 cache.StaticCache - get: object at key [[email protected]] ist abgelaufen
http-80-Processor263 cache.StaticCache - get: key [[email protected]] Rückgabewert [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor131 jsp.view-page - getSomeDataForEmail: innerhalb des Synchronisationsblocks
http-80-Processor131 cache.StaticCache - get: object at key [[email protected]] ist abgelaufen
http-80-Processor131 cache.StaticCache - get: key [[email protected]] Rückgabewert [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: innerhalb des Synchronisationsblocks
http-80-Processor104 cache.StaticCache - get: object at key [[email protected]] ist abgelaufen
http-80-Processor104 cache.StaticCache - get: key [[email protected]] Rückgabewert [null]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor283 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor2 jsp.view-page - getSomeDataForEmail: Der Synchronisationsblock wird aufgerufen
http-80-Processor2 jsp.view-page - getSomeDataForEmail: innerhalb des Synchronisationsblocks 

Ich wollte immer nur einen Thread sehen, der den Synchronisationsblock um die get/set -Operationen herum betrat/beendet.

Gibt es ein Problem beim Synchronisieren von String-Objekten? Ich dachte, der Cache-Schlüssel wäre eine gute Wahl, da er für die Operation eindeutig ist, und obwohl final String key innerhalb der Methode deklariert wurde, dachte ich, dass jeder Thread einen Verweis auf das gleiche Objekt und erhalten würde Daher würde die Synchronisation für dieses einzelne Objekt erfolgen.

Was mache ich hier falsch?

Update: Nach einem genaueren Blick auf die Protokolle scheint es sich um Methoden mit derselben Synchronisationslogik zu handeln, bei denen der Schlüssel immer derselbe ist, z. B. 

final String key = "blah";
...
synchronized(key) { ...

haben nicht das gleiche Parallelitätsproblem - nur jeweils ein Thread betritt den Block.

Update 2: Vielen Dank an alle für die Hilfe! Ich habe die erste Antwort zu intern()ing Strings akzeptiert, die mein ursprüngliches Problem gelöst hat - mehrere Threads gingen in synchronisierte Blöcke, wo ich dachte, dass sie dies nicht tun sollten, weil die key 's dasselbe hatten Wert.

Wie andere darauf hingewiesen haben, stellt sich die Verwendung von intern() für einen solchen Zweck und das Synchronisieren dieser Strings tatsächlich als eine schlechte Idee heraus - wenn JMeter-Tests gegen die Webapp ausgeführt werden, um die erwartete Last zu simulieren, sah ich, dass die verwendete Heap-Größe auf fast 1 GB anstieg in knapp 20 Minuten.

Derzeit verwende ich die einfache Lösung, nur die gesamte Methode zu synchronisieren - aber ich wirklichmag die von martinprobst und MBCook bereitgestellten Codebeispiele), aber da ich derzeit ungefähr 7 ähnliche getData()-Methoden in dieser Klasse habe (da sie etwa benötigt 7 verschiedene Daten aus einem Web - Service), ich wollte keine fast doppelte Logik zum Abrufen und Aufheben von Sperren für jede Methode hinzufügen, aber dies ist auf jeden Fall sehr, sehr wertvolle Informationen für die zukünftige Verwendung richtige Antworten, wie man eine Operation wie diesen Thread-sicherer macht, und ich würde mehr Stimmen für diese Antworten abgeben, wenn ich könnte!

40
matt b

Ohne mein Gehirn vollständig in Gang zu setzen, sieht es aus, als ob Sie Ihre Strings intern internieren müssen:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

Ansonsten sind zwei Zeichenfolgen mit demselben Wert nicht unbedingt dasselbe Objekt.

Beachten Sie, dass dies zu einem neuen Konfliktpunkt führen kann, da intern () tief in der VM möglicherweise eine Sperre erwerben muss. Ich habe keine Ahnung, wie moderne VMs in diesem Bereich aussehen, aber man hofft, dass sie teuflisch optimiert sind.

Ich gehe davon aus, dass Sie wissen, dass StaticCache noch Thread-sicher sein muss. Der Konflikt sollte jedoch winzig sein im Vergleich zu dem, was Sie hätten, wenn Sie den Cache sperren würden und nicht nur den Schlüssel beim Aufruf von getSomeDataForEmail.

Antwort auf das Fragen-Update :

Ich denke, das liegt daran, dass ein String-Literal immer dasselbe Objekt ergibt. Dave Costa weist in einem Kommentar darauf hin, dass es sogar noch besser ist: Ein Literal ergibt immer die kanonische Darstellung. Alle String-Literale mit dem gleichen Wert im gesamten Programm würden also dasselbe Objekt ergeben.

Bearbeiten

Andere haben darauf hingewiesen, dass das Synchronisieren von internen Strings tatsächlich eine wirklich schlechte Idee ist - zum Teil weil das Erstellen interner Strings dazu führen kann, dass sie auf Dauer existieren, und zum Teil weil, wenn in Ihrem Programm mehr als ein Bit Code synchronisiert wird In internen Zeichenfolgen bestehen Abhängigkeiten zwischen diesen Codebits. Das Verhindern von Deadlocks oder anderen Fehlern ist möglicherweise unmöglich.

Strategien, um dies zu vermeiden, indem ein Sperrobjekt pro Schlüsselzeichenfolge gespeichert wird, werden in anderen Antworten während der Eingabe entwickelt.

Hier ist eine Alternative - es verwendet immer noch eine einzelne Sperre, aber wir wissen, dass wir sowieso eine für den Cache benötigen werden, und Sie sprachen von 50 Threads, nicht von 5000, sodass dies nicht fatal sein könnte. Ich gehe auch davon aus, dass der Performance-Engpass hier langsam E/A in DoSlowThing () blockiert, was daher von der Serialisierung enorm profitieren wird. Wenn das nicht der Engpass ist, dann:

  • Wenn die CPU ausgelastet ist, reicht dieser Ansatz möglicherweise nicht aus und Sie benötigen einen anderen Ansatz.
  • Wenn die CPU nicht ausgelastet ist und der Zugriff auf den Server keinen Engpass darstellt, ist dieser Ansatz übertrieben, und Sie könnten sowohl diese als auch die Tastensperre vergessen, den gesamten Vorgang mit einem großen synchronisierten StaticCache (StaticCache) versehen es ist der einfache Weg.

Natürlich muss dieser Ansatz vor dem Einsatz auf Skalierbarkeit getestet werden - ich garantiere nichts.

Dieser Code erfordert NICHT, dass StaticCache synchronisiert oder anderweitig threadsicher ist. Dies muss erneut überprüft werden, wenn ein anderer Code (z. B. die geplante Bereinigung alter Daten) den Cache berührt.

IN_PROGRESS ist ein Dummy-Wert - nicht gerade sauber, aber der Code ist einfach und es erspart sich zwei Hashtables zu haben. Es behandelt nicht InterruptedException, da ich nicht weiß, was Ihre App in diesem Fall tun möchte. Wenn DoSlowThing () durchgehend für einen bestimmten Schlüssel fehlschlägt, ist dieser Code, wie er steht, nicht gerade elegant, da jeder durchlaufende Thread ihn erneut versucht. Da ich nicht weiß, was die Ausfallkriterien sind und ob sie vorübergehend oder dauerhaft sind, kann ich dies auch nicht tun. Ich stelle nur sicher, dass Threads nicht für immer blockieren. In der Praxis möchten Sie möglicherweise einen Datenwert in den Cache einfügen, der "nicht verfügbar" angibt, möglicherweise mit einem Grund und einem Timeout für den erneuten Versuch.

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

Jedes Mal, wenn etwas zum Cache hinzugefügt wird, werden alle Threads aktiviert und überprüfen den Cache (unabhängig von dem Schlüssel, nach dem sie gesucht werden). Daher ist es möglich, mit weniger umstrittenen Algorithmen eine bessere Leistung zu erzielen. Ein Großteil dieser Arbeit wird jedoch während Ihrer umfangreichen Leerlauf-CPU-Zeitblockierung für E/A ausgeführt. Daher ist dies möglicherweise kein Problem.

Dieser Code kann für die Verwendung mit mehreren Caches gemeinsam genutzt werden, wenn Sie geeignete Abstraktionen für den Cache und die zugehörige Sperre, die zurückgegebenen Daten, den Dummy IN_PROGRESS und die durchzuführende langsame Operation definieren. Das Ganze in eine Methode im Cache zu rollen, ist keine schlechte Idee.

38
Steve Jessop

Das Synchronisieren mit einem internen String ist möglicherweise überhaupt keine gute Idee - durch das Internieren wird der String zu einem globalen Objekt. Wenn Sie in verschiedenen Teilen Ihrer Anwendung dieselben internen Zeichenfolgen synchronisieren, werden Sie möglicherweise sehr seltsam im Grunde nicht zu behebende Synchronisationsprobleme wie Deadlocks. Es scheint unwahrscheinlich, aber wenn es passiert, sind Sie wirklich vermasselt. Generell sollten Sie immer nur ein lokales Objekt synchronisieren, bei dem Sie absolut sicher sind, dass kein Code außerhalb Ihres Moduls es sperren kann.

In Ihrem Fall können Sie eine synchronisierte Hashtabelle verwenden, um Sperrobjekte für Ihre Schlüssel zu speichern.

Z.B.:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

Dieser Code hat eine Race-Bedingung, bei der zwei Threads nacheinander ein Objekt in die Sperrtabelle einfügen können. Dies sollte jedoch kein Problem sein, da Sie nur noch einen weiteren Thread haben, der den Webservice aufruft und den Cache aktualisiert, was kein Problem sein sollte.

Wenn Sie den Cache nach einiger Zeit ungültig machen, sollten Sie nach dem Abrufen der Daten aus dem Cache erneut überprüfen, ob die Daten null sind, im Fall lock! = Null.

Alternativ und viel einfacher können Sie die gesamte Cache-Lookup-Methode ("getSomeDataByEmail") synchronisieren. Dies bedeutet, dass alle Threads synchronisiert werden müssen, wenn sie auf den Cache zugreifen. Dies kann ein Leistungsproblem sein. Versuchen Sie aber wie immer zuerst diese einfache Lösung und sehen Sie, ob es wirklich ein Problem ist! In vielen Fällen sollte dies nicht der Fall sein, da Sie wahrscheinlich viel mehr Zeit mit der Verarbeitung des Ergebnisses verbringen als mit der Synchronisierung.

25
Martin Probst

Strings sind nicht gute Kandidaten für die Synchronisation. Wenn Sie eine String-ID synchronisieren müssen, können Sie dies tun, indem Sie mit dem String einen Mutex erstellen (siehe " Synchronisierung mit einer ID "). Ob sich der Preis für diesen Algorithmus lohnt, hängt davon ab, ob für den Aufruf Ihres Dienstes erhebliche E/A erforderlich sind.

Ebenfalls:

  • Ich hoffe, dass die Methoden StaticCache.get () und set () threadsicher sind.
  • String.intern () ist mit einem Preis verbunden (einer, der zwischen VM -Implementierungen variiert) und sollte mit Vorsicht verwendet werden.
9
McDowell

Andere haben vorgeschlagen, die Zeichenketten zu internieren, und das wird funktionieren.

Das Problem ist, dass Java intern gespeicherte Zeichenfolgen beibehalten muss. Mir wurde gesagt, dass dies auch dann der Fall ist, wenn Sie keine Referenz besitzen, da der Wert bei der nächsten Verwendung dieser Zeichenfolge gleich sein muss. Dies bedeutet, dass das Internieren aller Zeichenfolgen anfängt, Speicher zu verbrauchen, was bei der von Ihnen beschriebenen Belastung ein großes Problem darstellen kann.

Ich habe zwei Lösungen dafür gesehen:

Sie könnten ein anderes Objekt synchronisieren

Erstellen Sie statt der E-Mail ein Objekt, das die E-Mail enthält (z. B. das Benutzerobjekt) und den Wert der E-Mail als Variable enthält. Wenn Sie bereits über ein anderes Objekt verfügen, das die Person darstellt (sagen Sie, dass Sie aufgrund der E-Mail-Adresse bereits etwas aus der Datenbank gezogen haben), können Sie dieses verwenden. Durch die Implementierung der Equals-Methode und der Hashcode-Methode können Sie sicherstellen, dass Java die Objekte als gleich betrachtet, wenn Sie einen statischen cache.contains () ausführen, um herauszufinden, ob sich die Daten bereits im Cache befinden (Sie müssen den Cache synchronisieren.) ).

Tatsächlich könnten Sie eine zweite Map für Objekte speichern, für die sie gesperrt werden soll. Etwas wie das:

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

Dadurch werden 15 Abrufe an derselben E-Mail-Adresse gleichzeitig verhindert. Sie benötigen etwas, um zu verhindern, dass zu viele Einträge in der emailLocks-Map landen. Dies würde es tun, wenn Sie LRUMap s von Apache Commons verwenden.

Dies erfordert einige Anpassungen, kann jedoch das Problem lösen.

Verwenden Sie einen anderen Schlüssel

Wenn Sie bereit sind, mögliche Fehler in Kauf zu nehmen (ich weiß nicht, wie wichtig dies ist), können Sie den Hashcode des Strings als Schlüssel verwenden. Ints müssen nicht interniert werden.

Zusammenfassung

Ich hoffe das hilft. Threading macht Spaß, oder? Sie können die Sitzung auch verwenden, um einen Wert festzulegen, der "Ich arbeite bereits daran, dies zu finden" bedeutet, und überprüfen, ob der zweite (dritte, N-te) Thread versuchen muss, das zu erstellen, oder warten, bis das Ergebnis angezeigt wird im Cache Ich hatte drei Vorschläge.

5
MBCook

Sie können die 1.5-Parallelitätsdienstprogramme verwenden, um einen Cache bereitzustellen, der mehrere gleichzeitige Zugriffe zulässt, und einen einzigen Anfügepunkt (d. H. Nur ein Thread, der das kostspielige Objekt "Erstellen" durchführt):

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

Offensichtlich behandelt dies keine Ausnahmen, wie Sie möchten, und der Cache hat keine eingebaute Zwangsräumung. Vielleicht könnten Sie dies jedoch als Grundlage für die Änderung Ihrer StaticCache-Klasse verwenden.

5
oxbow_lakes

Hier ist eine sichere kurze Java 8-Lösung, die eine Zuordnung dedizierter Sperrobjekte zur Synchronisierung verwendet:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Es hat den Nachteil, dass Schlüssel und Sperrobjekte für immer in der Karte verbleiben.

Dies lässt sich wie folgt umgehen:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key); // vulnerable to race-conditions
        }
    }
    return data;
}

Dann würden populäre Schlüssel ständig wieder in die Karte eingefügt, wobei Sperrobjekte neu zugewiesen werden.

Update : Dies lässt die Race-Condition-Möglichkeit, wenn zwei Threads gleichzeitig den synchronisierten Abschnitt für denselben Schlüssel eingeben, jedoch mit unterschiedlichen Sperren.

Es kann also sicherer und effizienter sein, den auslaufenden Guava-Cache zu verwenden :

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Beachten Sie, dass hier angenommen wird, dass StaticCache Thread-sicher ist und nicht unter gleichzeitigen Lese- und Schreibvorgängen für verschiedene Schlüssel leiden würde.

3
Vadzim

Verwenden Sie ein anständiges Caching-Framework wie ehcache

Das Implementieren eines guten Cache ist nicht so einfach, wie manche Leute glauben. 

In Bezug auf den Kommentar, dass String.intern () eine Quelle für Speicherverluste ist, ist dies eigentlich nicht wahr. Interned Strings sind Müll gesammelt, es kann nur länger dauern, da sie bei bestimmten JVMs (Sun) im Perm-Bereich gespeichert werden, der nur von vollständigen GCs berührt wird. 

2
kohlerm

Diese Frage erscheint mir etwas zu weitreichend und hat daher ebenso breite Antworten ausgelöst. Also werde ich versuchen, die Frage zu beantworten, von der ich umgeleitet wurde, leider wurde eines als Duplikat geschlossen.

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

Bei der (äußeren) lock-Operation wird die (innere) Sperre erworben, um für kurze Zeit exklusiven Zugriff auf die Karte zu erhalten, und wenn das entsprechende Objekt bereits in der Karte vorhanden ist, wartet der aktuelle Thread sonst auf es wird neue Condition in die Karte einfügen, die (innere) Sperre aufheben und fortfahren, und die (äußere) Sperre gilt als erhalten. Die (äußere) unlock-Operation, die zuerst eine (innere) Sperre erlangt, signalisiert Condition und entfernt dann das Objekt aus der Karte.

Die Klasse verwendet keine gleichzeitige Version von Map, da jeder Zugriff darauf durch eine einzige (innere) Sperre geschützt wird.

Bitte beachten Sie, dass die Semantik der lock()-Methode dieser Klasse anders ist als die von ReentrantLock.lock(). Die wiederholten lock()-Aufrufe ohne gepaarte unlock() werden den aktuellen Thread unbegrenzt aufhängen.

Ein Anwendungsbeispiel, das möglicherweise auf die Situation anwendbar ist, beschreibt das OP

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }
2
igor.zh

Der Anruf:

   final String key = "Data-" + email;

erstellt bei jedem Aufruf der Methode ein neues Objekt. Da dieses Objekt für das Sperren verwendet wird und bei jedem Aufruf dieser Methode ein neues Objekt erstellt wird, synchronisieren Sie den Zugriff auf die Karte nicht wirklich anhand des Schlüssels.

Dies erklärt deine Bearbeitung weiter. Wenn Sie eine statische Zeichenfolge haben, funktioniert sie.

Die Verwendung von intern () löst das Problem, da der String aus einem internen Pool der String-Klasse zurückgegeben wird. Dadurch wird sichergestellt, dass der String im Pool verwendet wird, wenn zwei Strings gleich sind. Sehen

http://Java.Sun.com/j2se/1.4.2/docs/api/Java/lang/String.html#intern ()

2
Mario Ortegón

Ihr Hauptproblem ist nicht nur, dass es mehrere Instanzen von String mit demselben Wert gibt. Das Hauptproblem besteht darin, dass Sie nur einen Monitor benötigen, auf dem synchronisiert werden soll, um auf das StaticCache-Objekt zuzugreifen. Andernfalls könnten mehrere Threads gleichzeitig StaticCache ändern (allerdings unter verschiedenen Schlüsseln), was höchstwahrscheinlich keine gleichzeitige Änderung unterstützt.

2
Alexander

Das ist ziemlich spät, aber hier wird ziemlich viel falscher Code angezeigt.

In diesem Beispiel:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

Die Synchronisierung hat einen falschen Bereich. Für einen statischen Cache, der eine Get/Put-API unterstützt, sollte mindestens eine Synchronisierung um die Operationen get und getIfAbsentPut erfolgen, um einen sicheren Zugriff auf den Cache zu erhalten. Der Umfang der Synchronisierung wird der Cache selbst sein.

Wenn Aktualisierungen an den Datenelementen selbst vorgenommen werden müssen, wird eine zusätzliche Synchronisierungsebene hinzugefügt, die sich auf die einzelnen Datenelemente beziehen sollte.

SynchronizedMap kann anstelle der expliziten Synchronisation verwendet werden. Vorsicht ist jedoch geboten. Wenn die falschen APIs verwendet werden (get und put anstelle von putIfAbsent), haben die Operationen trotz der Verwendung der synchronisierten Map nicht die erforderliche Synchronisierung. Beachten Sie die durch die Verwendung von putIfAbsent verursachten Komplikationen: Entweder muss der Put-Wert berechnet werden, selbst wenn er nicht benötigt wird (weil der Put nicht wissen kann, ob der Put-Wert erforderlich ist, bis der Inhalt des Caches überprüft wird) oder dass Vorsicht geboten ist Verwendung von Delegierung (beispielsweise mit Future, das funktioniert, ist aber etwas nicht übereinstimmend; siehe unten), wobei der Put-Wert bei Bedarf bei Bedarf erzielt wird.

Die Verwendung von Futures ist möglich, erscheint jedoch etwas umständlich und vielleicht ein bisschen übertrieben. Die Future-API ist für asynchrone Vorgänge, insbesondere für Vorgänge, die möglicherweise nicht sofort abgeschlossen werden, im Kern. Die Einbindung von Future fügt sehr wahrscheinlich eine Ebene zur Erstellung von Threads hinzu - möglicherweise auch unnötige Komplikationen.

Das Hauptproblem bei der Verwendung von Future für diese Art von Operation besteht darin, dass Future inhärent mit Multithreading zusammenhängt. Die Verwendung von Future, wenn ein neuer Thread nicht erforderlich ist, bedeutet, viele der Maschinen von Future zu ignorieren, wodurch sie zu einer zu schweren API für diese Verwendung wird.

1
Thomas Bitonti

Letzte Aktualisierung 2019,

Wenn Sie nach neuen Möglichkeiten suchen, um die Synchronisation in Java zu implementieren , ist diese Antwort genau das Richtige für Sie.

enter image description here

Ich fand diesen erstaunlichen Blog von Anatoliy Korovin, der Ihnen helfen wird, die Synchronisation tief zu verstehen.

So synchronisieren Sie Blöcke nach dem Wert des Objekts in Java .

Dies hat mir geholfen zu hoffen, dass auch neue Entwickler dies nützlich finden.

0

Warum nicht einfach eine statische HTML-Seite rendern, die alle x Minuten an den Benutzer geliefert und neu generiert wird?

0
MattW.

auf andere Weise wird das String-Objekt synchronisiert: 

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if(obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if(obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}
0
celen

In deinem Fall könntest du so etwas benutzen (das leckt keinen Speicher):

private Synchronizer<String> synchronizer = new Synchronizer();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;

    return synchronizer.synchronizeOn(key, () -> {

        SomeData[] data = (SomeData[]) StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
        } else {
          logger.debug("getSomeDataForEmail: using cached object");
        }
        return data;

    });
}

um es zu benutzen, fügen Sie einfach eine Abhängigkeit hinzu:

compile 'com.github.matejtymes:javafixes:1.3.0'
0
Matej Tymes

Ich würde auch vorschlagen, die String-Verkettung vollständig zu entfernen, wenn Sie sie nicht brauchen.

final String key = "Data-" + email;

Gibt es andere Dinge/Objekttypen im Cache, die die E-Mail-Adresse verwenden, für die Sie das zusätzliche "Data-" am Anfang des Schlüssels benötigen?

wenn nicht, würde ich das einfach machen 

final String key = email;

und Sie vermeiden auch die zusätzliche Erstellung von Strings.

0
John Gardner

Ich habe eine kleine Sperrklasse hinzugefügt, die für jeden Schlüssel einschließlich Strings gesperrt/synchronisiert werden kann.

Siehe Implementierung für Java 8, Java 6 und einen kleinen Test.

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

public class DynamicKeyLock implementiert Lock { private final static ConcurrentHashMap locksMap = new ConcurrentHashMap (); private final T-Schlüssel;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Prüfung:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}
0

Falls andere ein ähnliches Problem haben, funktioniert der folgende Code, soweit ich das beurteilen kann:

import Java.util.Map;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

Im Falle des OP würde es so verwendet werden:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

Wenn aus dem synchronisierten Code nichts zurückgegeben werden soll, kann die Synchronisierungsmethode folgendermaßen geschrieben werden:

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}
0
ragnaroh