2010-09-03 10 views
33

Angenommen, wir haben in unserer Anwendung ein CountryList-Objekt, das die Länderliste zurückgeben soll. Das Laden von Ländern ist eine schwere Operation, daher sollte die Liste zwischengespeichert werden.Thread-sicherer Cache eines Objekts in Java

Zusätzliche Anforderungen:

  • Country sollte
  • Country Thread-sicher sein sollte faul laden (nur auf Anfrage)
  • Country sollte die Ungültigkeit des Cache
  • Country unterstützen sollte unter Berücksichtigung optimiert werden dass der Cache sehr selten ungültig gemacht wird

Ich kam wi th die folgende Lösung:

public class CountryList { 
    private static final Object ONE = new Integer(1); 

    // MapMaker is from Google Collections Library  
    private Map<Object, List<String>> cache = new MapMaker() 
     .initialCapacity(1) 
     .makeComputingMap(
      new Function<Object, List<String>>() { 
       @Override 
       public List<String> apply(Object from) { 
        return loadCountryList(); 
       } 
      }); 

    private List<String> loadCountryList() { 
     // HEAVY OPERATION TO LOAD DATA 
    } 

    public List<String> list() { 
     return cache.get(ONE); 
    } 

    public void invalidateCache() { 
     cache.remove(ONE); 
    } 
} 

Was denken Sie darüber? Siehst du etwas Schlechtes daran? Gibt es einen anderen Weg, es zu tun? Wie kann ich es besser machen? Sollte ich in diesen Fällen nach einer völlig anderen Lösung suchen?

Danke.

+2

Ich bin nicht davon überzeugt, dass dies Thread-sicher ist. Was passiert, wenn zwei Threads invalidateCache() gleichzeitig aufrufen oder einer die list() gleichzeitig aufruft, während ein anderer invalidateCache() aufruft? – ChrisH

+1

MapMaker gibt eine thread-sichere Implementierung der Map-Schnittstelle zurück. makeComputingMap() führt die Berechnung atomar aus (http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/collect/MapMaker.html#makeComputingMap(com.google.common.base.Function)).) –

+0

Möchten Sie eine Antwort akzeptieren? –

Antwort

15

Dank Sie alle Jungs, vor allem für Benutzer " gid "wer gab die Idee.

Mein Ziel war es, die Leistung für die Operation get() zu optimieren, da die Operation invalidate() sehr selten genannt wird.

Ich schrieb eine Test-Klasse, die 16 Threads startet, jeder ruft Get() - Operation eine Million Mal. Mit dieser Klasse habe ich eine Implementierung auf meiner 2-Kern-Maschine beschrieben.

Prüfergebnisse

Implementation    Time 
no synchronisation   0,6 sec 
normal synchronisation  7,5 sec 
with MapMaker    26,3 sec 
with Suppliers.memoize  8,2 sec 
with optimized memoize  1,5 sec 

1) „Keine Synchronisation“ ist nicht Thread-sicher, sondern gibt uns die beste Leistung, die wir vergleichen können.

@Override 
public List<String> list() { 
    if (cache == null) { 
     cache = loadCountryList(); 
    } 
    return cache; 
} 

@Override 
public void invalidateCache() { 
    cache = null; 
} 

2) "Normale Synchronisation" - ziemlich gut Performace, Standard no-brainer Implementierung

@Override 
public synchronized List<String> list() { 
    if (cache == null) { 
     cache = loadCountryList(); 
    } 
    return cache; 
} 

@Override 
public synchronized void invalidateCache() { 
    cache = null; 
} 

3) "mit Map Maker" - sehr schlechte Leistung.

Siehe meine Frage an der Spitze für den Code.

4) "mit Suppliers.memoize" - gute Leistung. Aber da die Performance die gleiche "normale Synchronisation" hat, müssen wir sie optimieren oder einfach die "normale Synchronisation" verwenden.

Siehe die Antwort des Benutzers "GID" für Code.

5) "mit optimierten Memoize" - die Leistung vergleichbar mit "keine Synchronisierung" -Einführung, aber Thread-Safe. Das ist die eine, die wir brauchen.

Die Cache-Klasse selbst: (. Der Lieferant Schnittstellen hier ist von Google Sammlungen Bibliothek und es hat nur eine Methode get() sieht http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/base/Supplier.html)

public class LazyCache<T> implements Supplier<T> { 
    private final Supplier<T> supplier; 

    private volatile Supplier<T> cache; 

    public LazyCache(Supplier<T> supplier) { 
     this.supplier = supplier; 
     reset(); 
    } 

    private void reset() { 
     cache = new MemoizingSupplier<T>(supplier); 
    } 

    @Override 
    public T get() { 
     return cache.get(); 
    } 

    public void invalidate() { 
     reset(); 
    } 

    private static class MemoizingSupplier<T> implements Supplier<T> { 
     final Supplier<T> delegate; 
     volatile T value; 

     MemoizingSupplier(Supplier<T> delegate) { 
      this.delegate = delegate; 
     } 

     @Override 
     public T get() { 
      if (value == null) { 
       synchronized (this) { 
        if (value == null) { 
         value = delegate.get(); 
        } 
       } 
      } 
      return value; 
     } 
    } 
} 

Beispiel für die Verwendung:

public class BetterMemoizeCountryList implements ICountryList { 

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){ 
     @Override 
     public List<String> get() { 
      return loadCountryList(); 
     } 
    }); 

    @Override 
    public List<String> list(){ 
     return cache.get(); 
    } 

    @Override 
    public void invalidateCache(){ 
     cache.invalidate(); 
    } 

    private List<String> loadCountryList() { 
     // this should normally load a full list from the database, 
     // but just for this instance we mock it with: 
     return Arrays.asList("Germany", "Russia", "China"); 
    } 
} 
+1

können Sie bitte erklären, warum die optimierte Version schneller ist? Versuchen Sie auch mit diesem Beispiel http://stackoverflow.com/a/3637441/638670 Der Unterschied zwischen dieser Version und Ihrer synchronisierten Version besteht darin, dass Sie die gesamte Methode synchronisieren, während Sie nur den synchronisierten Zugriff auf das Cache-Objekt benötigen . – Nerrve

0

Das sieht ok für mich aus (ich nehme an, MapMaker ist von Google Sammlungen?) Idealerweise würden Sie keine Karte verwenden müssen, weil Sie keine Schlüssel wirklich haben, aber da die Implementierung von irgendwelchen Anrufern nicht verborgen ist sehe das als eine große Sache.

5

Immer wenn ich etwas zwischenspeichern muss, verwende ich gerne die Proxy pattern. Mit diesem Muster zu tun bietet eine Trennung von Bedenken. Ihr ursprüngliches Objekt kann sich mit dem verzögerten Laden befassen. Ihr Proxy (oder Guardian) -Objekt kann für die Überprüfung des Caches zuständig sein.

Im Detail:

  • ein Objekt Country Klasse definieren, die threadsicher ist, vorzugsweise unter Verwendung von Synchronisations-Blöcken oder andere semaphore Schleusen.
  • Extrahieren Sie die Schnittstelle dieser Klasse in eine CountryQueryable-Schnittstelle.
  • Definieren Sie ein anderes Objekt, CountryListProxy, das das CountryQueryable implementiert.
  • Lassen Sie den CountryListProxy nur instanziiert werden und erlauben Sie nur, dass er über seine Schnittstelle auf referenziert wird.

Von hier aus können Sie Ihre Cache-Invalidierungsstrategie in das Proxy-Objekt einfügen. Speichern Sie die Zeit des letzten Ladevorgangs, und vergleichen Sie bei der nächsten Anforderung zum Anzeigen der Daten die aktuelle Uhrzeit mit der Cachezeit. Definieren Sie eine Toleranzstufe, bei der die Daten neu geladen werden, wenn zu viel Zeit verstrichen ist.

Soweit Lazy Load, beziehen Sie sich auf here.

Jetzt für ein paar guten Down-Home Beispielcode:

public interface CountryQueryable { 

    public void operationA(); 
    public String operationB(); 

} 

public class CountryList implements CountryQueryable { 

    private boolean loaded; 

    public CountryList() { 
     loaded = false; 
    } 

    //This particular operation might be able to function without 
    //the extra loading. 
    @Override 
    public void operationA() { 
     //Do whatever. 
    } 

    //This operation may need to load the extra stuff. 
    @Override 
    public String operationB() { 
     if (!loaded) { 
      load(); 
      loaded = true; 
     } 

     //Do whatever. 
     return whatever; 
    } 

    private void load() { 
     //Do the loading of the Lazy load here. 
    } 

} 

public class CountryListProxy implements CountryQueryable { 

    //In accordance with the Proxy pattern, we hide the target 
    //instance inside of our Proxy instance. 
    private CountryQueryable actualList; 
    //Keep track of the lazy time we cached. 
    private long lastCached; 

    //Define a tolerance time, 2000 milliseconds, before refreshing 
    //the cache. 
    private static final long TOLERANCE = 2000L; 

    public CountryListProxy() { 
      //You might even retrieve this object from a Registry. 
     actualList = new CountryList(); 
     //Initialize it to something stupid. 
     lastCached = Long.MIN_VALUE; 
    } 

    @Override 
    public synchronized void operationA() { 
     if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) { 
      //Refresh the cache. 
        lastCached = System.getCurrentTimeMillis(); 
     } else { 
      //Cache is okay. 
     } 
    } 

    @Override 
    public synchronized String operationB() { 
     if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) { 
      //Refresh the cache. 
        lastCached = System.getCurrentTimeMillis(); 
     } else { 
      //Cache is okay. 
     } 

     return whatever; 
    } 

} 

public class Client { 

    public static void main(String[] args) { 
     CountryQueryable queryable = new CountryListProxy(); 
     //Do your thing. 
    } 

} 
+0

können Sie einen Beispielcode angeben? –

+0

Posted einige. :) – Mike

0

Das ist viel zu einfach, die ComputingMap Sachen zu verwenden. Sie brauchen nur eine einfache Implementierung, bei der alle Methoden synchronisiert sind, und Sie sollten in Ordnung sein.Dies blockiert natürlich den ersten Thread, der es trifft (und es bekommt), und ein anderer Thread trifft es, während der erste Thread den Cache lädt (und dasselbe, wenn jemand die InvalidateCache-Sache aufruft - wo Sie auch entscheiden sollten, ob der InvalidateCache den Cache laden soll Cache neu, oder einfach null es, lassen Sie den ersten Versuch, es wieder zu blockieren), aber dann sollten alle Threads gut durchgehen.

1

Ich bin mir nicht sicher, wofür die Karte ist. Wenn ich ein fauler, zwischengespeicherte Objekt brauchen, ich kann es dies in der Regel mögen:

public class CountryList 
{ 
    private static List<Country> countryList; 

    public static synchronized List<Country> get() 
    { 
    if (countryList==null) 
     countryList=load(); 
    return countryList; 
    } 
    private static List<Country> load() 
    { 
    ... whatever ... 
    } 
    public static synchronized void forget() 
    { 
    countryList=null; 
    } 
} 

Ich denke, das ähnlich ist, was Sie tun, aber ein wenig einfacher. Wenn Sie die Karte und das ONE benötigen, das Sie für die Frage vereinfacht haben, okay.

Wenn Sie es threadsicher wollen, sollten Sie das get und das forget synchronisieren.

+0

@iimuhin: Weil sie nur auf statische Daten wirken. Diese Klasse enthält keine Instanzdaten. Immer wenn eine Funktion statisch sein kann, mache ich sie statisch. Dies ist etwas effizienter und dient dem Leser als Dokumentation. – Jay

1

Was denken Sie darüber? Siehst du etwas Schlechtes daran?

Bleah - Sie sind eine komplexe Datenstruktur, Map Maker, mit mehreren Funktionen (Karte Zugang, Concurrency freundlicher Zugang, latenter Bau von Werten, usw.) aufgrund eines einzigen Merkmals sind Sie nach (latenter Schaffung eines einzelnes konstruktions-teures Objekt).

Während die Wiederverwendung von Code ein gutes Ziel ist, fügt dieser Ansatz zusätzlichen Overhead und Komplexität hinzu. Darüber hinaus führt es zukünftige Maintainer in die Irre, wenn sie eine Kartendatenstruktur darin sehen, dass es dort eine Karte von Schlüsseln/Werten gibt, wenn es wirklich nur eine Sache gibt (Liste von Ländern). Einfachheit, Lesbarkeit und Klarheit sind der Schlüssel für die Wartbarkeit in der Zukunft.

Gibt es einen anderen Weg, es zu tun? Wie kann ich es besser machen? Sollte ich in diesen Fällen nach einer völlig anderen Lösung suchen?

Scheint so, als ob Sie nach dem Lazy-Loading sind. Sehen Sie sich Lösungen für andere SO-Lazy-Loading-Fragen an. Zum Beispiel deckt dieser den klassischen Vier-Augen-Ansatz (stellen Sie sicher, dass Sie mit Java 1.5 oder höher):

How to solve the "Double-Checked Locking is Broken" Declaration in Java?

Anstatt nur einfach den Lösungscode hier wiederholen, ich denke, es ist nützlich zu lesen die Diskussion über Lazy Loading durch Doppel-Check dort um Ihre Wissensdatenbank zu erweitern. (Entschuldigung, wenn das pompös abläuft - versuche einfach zu angeln statt zu füttern bla bla bla ...)

1

Dort gibt es eine Bibliothek (von atlassian) - eine der util-Klassen namens LazyReference. LazyReference ist eine Referenz auf ein Objekt, das träge erstellt werden kann (beim ersten Aufruf). Es ist gewährleistet, thread sicher, und die Init ist auch garantiert nur einmal auftreten - wenn zwei Threads ruft() gleichzeitig, ein Thread wird berechnet, der andere Thread blockiert warten.

see a sample code:

final LazyReference<MyObject> ref = new LazyReference() { 
    protected MyObject create() throws Exception { 
     // Do some useful object construction here 
     return new MyObject(); 
    } 
}; 

//thread1 
MyObject myObject = ref.get(); 
//thread2 
MyObject myObject = ref.get(); 
+0

netter Tipp, aber diese Klasse unterstützt keine kontrollierte Cache-Invalidierung. –

+0

ah das ist wahr. – Chii

0

Verwenden Sie die Initialization on demand holder idiom

public class CountryList { 
    private CountryList() {} 

    private static class CountryListHolder { 
    static final List<Country> INSTANCE = new List<Country>(); 
    } 

    public static List<Country> getInstance() { 
    return CountryListHolder.INSTANCE; 
    } 

    ... 
} 
31

google Sammlungen liefert eigentlich genau das Richtige für gerade diese Art der Sache: Supplier

Ihr Code so etwas wie sein würde:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){ 
    public List<String> get(){ 
     return loadCountryList(); 
    } 
}; 


// volatile reference so that changes are published correctly see invalidate() 
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier); 


public List<String> list(){ 
    return memorized.get(); 
} 

public void invalidate(){ 
    memorized = Suppliers.memoize(supplier); 
} 
+0

nett ein. Vielen Dank. Ich habe es gerade getestet. Dies verbessert die Leistung wirklich. –

+0

Was ist eine "Verletzung" Referenz :-)? – helpermethod

+1

Das Schlüsselwort ist "flüchtig". –

1

Ihre Bedürfnisse scheinen hier ziemlich einfach zu sein. Die Verwendung von MapMaker macht die Implementierung komplizierter als nötig. Das ganze doppelt überprüfte Sperr-Idiom ist schwierig zu korrigieren und funktioniert nur mit 1.5+. Und um ehrlich zu sein, ist es eine der wichtigsten Regeln der Programmierung zu brechen:

Vorzeitige Optimierung ist die Wurzel alle Übel .

Das doppelt überprüfte Sperr-Idiom versucht, die Synchronisationskosten zu vermeiden, wenn der Cache bereits geladen ist. Aber verursacht dieser Overhead wirklich Probleme? Ist es die Kosten für komplexeren Code wert? Ich nehme an, es ist nicht so, bis das Profiling Ihnen etwas anderes sagt.

Hier ist eine sehr einfache Lösung, die keinen 3rd-Party-Code erfordert (die JCIP-Anmerkung ignorierend). Es wird davon ausgegangen, dass eine leere Liste bedeutet, dass der Cache noch nicht geladen wurde. Außerdem wird verhindert, dass der Inhalt der Länderliste in den Clientcode übergeht, der möglicherweise die zurückgegebene Liste ändern könnte. Wenn dies für Sie kein Problem darstellt, können Sie den Aufruf von Collections.unmodifiedList() entfernen.

public class CountryList { 

    @GuardedBy("cache") 
    private final List<String> cache = new ArrayList<String>(); 

    private List<String> loadCountryList() { 
     // HEAVY OPERATION TO LOAD DATA 
    } 

    public List<String> list() { 
     synchronized (cache) { 
      if(cache.isEmpty()) { 
       cache.addAll(loadCountryList()); 
      } 
      return Collections.unmodifiableList(cache); 
     } 
    } 

    public void invalidateCache() { 
     synchronized (cache) { 
      cache.clear(); 
     } 
    } 

} 
0

Follow-up zu Mikes Lösung oben. Mein Kommentar zu formatieren, nicht erwartet ... :(

Achten Sie auf Probleme bei der Synchronisierung in operationB, zumal Last() ist langsam:

public String operationB() { 
    if (!loaded) { 
     load(); 
     loaded = true; 
    } 

    //Do whatever. 
    return whatever; 
} 

Man könnte es auf diese Weise beheben:

public String operationB() { 
    synchronized(loaded) { 
     if (!loaded) { 
      load(); 
      loaded = true; 
     } 
    } 

    //Do whatever. 
    return whatever; 
} 

stellen Sie sicher immer bei jedem Zugriff auf die geladene Variable synchronisieren.

+0

Sie können nicht auf einem Grundelement synchronisieren. Sie können nur für Objekte synchronisieren. –

Verwandte Themen