2009-11-26 3 views
55

Java WeakHashMap viene spesso citato come utile per la memorizzazione nella cache. Sembra strano però che i suoi deboli riferimenti siano definiti in termini di chiavi della mappa, non dei suoi valori. Voglio dire, sono i valori che voglio memorizzare nella cache e che voglio ottenere dei garbage collection una volta che nessun altro al di fuori della cache fa un forte riferimento a loro, no?Java's WeakHashMap e memorizzazione nella cache: perché fa riferimento alle chiavi, non ai valori?

In che modo aiuta a contenere riferimenti deboli ai tasti? Se fai un ExpensiveObject o = weakHashMap.get("some_key"), allora voglio che la cache resti su "o" finché il chiamante non ha più il riferimento forte, e non mi interessa affatto dell'oggetto stringa "some_key".

Mi manca qualcosa?

+0

API Java è pieno di stranezze strane. Puoi sempre riscrivere WeakHashMap usando WeakReference. – Pacerier

risposta

98

WeakHashMap non è utile come cache, almeno come la maggior parte della gente ci pensa. Come dici tu, usa le chiavi, non deboli , non deboli valori, quindi non è progettato per quello che la maggior parte della gente vuole usarlo (e, in effetti, ho visto che lo si utilizza per, erroneamente).

WeakHashMap è principalmente utile per mantenere i metadati relativi agli oggetti il ​​cui ciclo di vita non è controllato. Ad esempio, se hai un gruppo di oggetti che passano attraverso la tua classe, e vuoi tenere traccia dei dati extra su di loro senza bisogno di essere avvisati quando escono dal campo di applicazione, e senza il tuo riferimento per mantenerli vivi.

Un semplice esempio (e uno ho usato prima) potrebbe essere qualcosa di simile:

WeakHashMap<Thread, SomeMetaData> 

dove si potrebbe tenere traccia di ciò che le discussioni varie nel sistema stanno facendo; quando il thread muore, la voce verrà rimossa silenziosamente dalla tua mappa e non manterrai il filo recuperato dalla garbage collection se sei l'ultimo riferimento ad esso. È quindi possibile scorrere le voci in quella mappa per scoprire quali metadati si hanno sui thread attivi nel proprio sistema.

Vedere WeakHashMap in not a cache! per ulteriori informazioni.

Per il tipo di cache che stai cercando, utilizza un sistema di cache dedicato (ad esempio EHCache) o guarda google-collections'MapMaker class; qualcosa come

new MapMaker().weakValues().makeMap(); 

farà quello che stai dopo, o se si desidera ottenere fantasia è possibile aggiungere scadenza temporizzata:

new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap(); 
+4

Solo per aggiornarlo per agosto 2013: Google Collections è ora denominato Guava e la logica di creazione della cache è ora parte del [CacheBuilder] (http://docs.guava-libraries.googlecode.com/git-history/release/ javadoc/index.html) classe. –

+0

Link più preciso: http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/cache/CacheBuilder.html –

+1

Nota, penso che nel tuo esempio per MapMaker eri intendeva dire nuovo MapMaker(). softValues ​​(). makeMap(), come chiamare weakValues ​​() ti dà lo stesso risultato di una WeakHashMap. C'è un ottimo esempio qui su come costruire una cache con MapMaker - http://stackoverflow.com/questions/3737140/use-of-google-collections-mapmaker – jklp

30

L'uso principale per WeakHashMap è quando si hanno mappature che si desidera scomparire quando le loro chiavi scompaiono. Una cache è il contrario --- hai mappature che vuoi sparire quando i loro valori scompaiono.

Per una cache, quello che si desidera è un Map<K,SoftReference<V>>. Uno SoftReference verrà raccolto automaticamente quando la memoria diventa stretta. (Contrasto con uno WeakReference, che può essere cancellato non appena non c'è più un riferimento difficile al suo referente.) Vuoi che i tuoi riferimenti siano soft in una cache (almeno in uno in cui i mapping dei valori-chiave non vanno stantio), da allora c'è la possibilità che i tuoi valori siano ancora nella cache se li cerchi in seguito. Se invece i riferimenti fossero deboli, i tuoi valori verrebbero immediatamente recuperati, vanificando lo scopo del caching.

Per comodità, si potrebbe desiderare di nascondere i valori SoftReference all'interno del vostro Map implementazione, in modo che la cache sembra essere di tipo <K,V> invece di <K,SoftReference<V>>. Se si desidera fare ciò, this question ha suggerimenti per le implementazioni disponibili in rete.

Si noti inoltre che quando si usa SoftReference valori in un Map, è necessario fare qualcosa per rimuovere le coppie chiave-valore, che hanno avuto il loro SoftReferences cancellati --- altrimenti il ​​vostro Map perderà la memoria.

+0

utilizzando questa soluzione nel tempo ti lascia con molti elementi di hashmap che il valore è stato gc-ed. c'è un'alternativa che usi un approccio simile? –

+0

Un approccio 'Map > lascia le istanze di' SoftReference' nella mappa che contengono i referenti 'null' dopo l'esecuzione di GC. Penso che l'implementazione interna di questa mappa debba periodicamente cancellare tutti i mapping con un valore che è un riferimento soft con un riferimento "null", per una bella pulizia. – Timmos

+2

(continua) In senso stretto, se un programmatore ingenuo usa l'implementazione 'HashMap >' allora questo causerà una perdita di memoria. Potresti considerare di includere questo nella tua risposta. Date un'occhiata a come 'WeakHashMap' lo fa, Oracle JDK ha un metodo privato' expungeStaleEntries' in esso che si occupa di questa pulizia. – Timmos

6

Un'altra cosa da considerare è che se si utilizza l'approccio Map<K, WeakReference<V>>, il valore potrebbe scomparire, ma il mapping non lo farà. A seconda dell'utilizzo, si può finire con una mappa contenente molte voci i cui riferimenti deboli sono stati sottoposti a GC.

+0

'Mappa ', non 'Mappa >'. Questa risposta sembra avere un senso in superficie, ma nota che i mapping mancanti possono essere rimossi ogni volta che l'utente chiama 'Map.get', e vedendo che [questo è esattamente il modo in cui WeakHashMap rimuove le chiavi] (http://archive.is/ Z3aK9 # selection-1069.117-1069.237), non poteva essere che il team di Java non lo avesse capito. – Pacerier

6

Sono necessarie due mappe: una che mappa tra la chiave della cache ei valori weak referenced e una nella direzione della direzione opposta tra i valori di riferimento deboli e le chiavi. E hai bisogno di un reference queue e di un thread di pulizia.

I riferimenti deboli hanno la possibilità di spostare il riferimento in una coda quando l'oggetto di riferimento non è più accessibile. Questa coda deve essere svuotata da un thread di pulizia. E per la pulizia è necessario ottenere la chiave per un riferimento. Questo è il motivo per cui è richiesta la seconda mappa.

L'esempio seguente mostra come creare una cache con una mappa hash di riferimenti deboli. Quando si esegue il programma si ottiene il seguente output:

 
$ javac -Xlint:unchecked Cache.java && java Cache 
{even: [2, 4, 6], odd: [1, 3, 5]} 
{even: [2, 4, 6]} 

La prima riga mostra il contenuto della cache prima del riferimento alla lista di strano è stato eliminato e la seconda linea dopo le probabilità sono stati cancellati.

Questo è il codice:

import java.lang.ref.Reference; 
import java.lang.ref.ReferenceQueue; 
import java.lang.ref.WeakReference; 
import java.util.Arrays; 
import java.util.Collections; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 

class Cache<K,V> 
{ 
    ReferenceQueue<V> queue = null; 
    Map<K,WeakReference<V>> values = null; 
    Map<WeakReference<V>,K> keys = null; 
    Thread cleanup = null; 

    Cache() 
    { 
     queue = new ReferenceQueue<V>(); 
     keys = Collections.synchronizedMap (new HashMap<WeakReference<V>,K>()); 
     values = Collections.synchronizedMap (new HashMap<K,WeakReference<V>>()); 
     cleanup = new Thread() { 
       public void run() { 
        try { 
         for (;;) { 
          @SuppressWarnings("unchecked") 
          WeakReference<V> ref = (WeakReference<V>)queue.remove(); 
          K key = keys.get(ref); 
          keys.remove(ref); 
          values.remove(key); 
         } 
        } 
        catch (InterruptedException e) {} 
       } 
      }; 
     cleanup.setDaemon (true); 
     cleanup.start(); 
    } 

    void stop() { 
     cleanup.interrupt(); 
    } 

    V get (K key) { 
     return values.get(key).get(); 
    } 

    void put (K key, V value) { 
     WeakReference<V> ref = new WeakReference<V>(value, queue); 
     keys.put (ref, key); 
     values.put (key, ref); 
    } 

    public String toString() { 
     StringBuilder str = new StringBuilder(); 
     str.append ("{"); 
     boolean first = true; 
     for (Map.Entry<K,WeakReference<V>> entry : values.entrySet()) { 
      if (first) 
       first = false; 
      else 
       str.append (", "); 
      str.append (entry.getKey()); 
      str.append (": "); 
      str.append (entry.getValue().get()); 
     } 
     str.append ("}"); 
     return str.toString(); 
    } 

    static void gc (int loop, int delay) throws Exception 
    { 
     for (int n = loop; n > 0; n--) { 
      Thread.sleep(delay); 
      System.gc(); // <- obstinate donkey 
     } 
    } 

    public static void main (String[] args) throws Exception 
    { 
     // Create the cache 
     Cache<String,List> c = new Cache<String,List>(); 

     // Create some values 
     List odd = Arrays.asList(new Object[]{1,3,5}); 
     List even = Arrays.asList(new Object[]{2,4,6}); 

     // Save them in the cache 
     c.put ("odd", odd); 
     c.put ("even", even); 

     // Display the cache contents 
     System.out.println (c); 

     // Erase one value; 
     odd = null; 

     // Force garbage collection 
     gc (10, 10); 

     // Display the cache again 
     System.out.println (c); 

     // Stop cleanup thread 
     c.stop(); 
    } 
} 
+1

Ottima risposta. Vale la pena notare che un ReferenceQueue, a differenza di molti altri tipi di raccolta, blocca fino a quando un valore può essere restituito da queue.remove(). Ciò significa che il thread di pulitura non è il ciclo infinito non in attesa che il primo sguardo potrebbe suggerire. –

+0

@Gordon Se si utilizzano chiavi deboli e valori deboli, tutto è debole e verrà eliminato automaticamente dopo averlo aggiunto alla cache. – ceving

+0

** Questa è la risposta **. Quindi possiamo dire che è per ** ottimizzare la fase di pulizia ** che l'API è implementata come tale. – Pacerier