Mega Code Archive

 
Categories / Java / Collections Data Structure
 

Soft ValueMap

/* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.  *   * This program and the accompanying materials are made available under  * the terms of the Common Public License v1.0 which accompanies this distribution,  * and is available at http://www.eclipse.org/legal/cpl-v10.html  *   * $Id: SoftValueMap.java,v 1.1.1.1 2004/05/09 16:57:55 vlad_r Exp $  */ import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.Collection; import java.util.Map; import java.util.Set; // ---------------------------------------------------------------------------- /**  * MT-safety: an instance of this class is <I>not</I> safe for access from  * multiple concurrent threads [even if access is done by a single thread at a  * time]. The caller is expected to synchronize externally on an instance [the  * implementation does not do internal synchronization for the sake of efficiency].  * java.util.ConcurrentModificationException is not supported either.  *  * @author (C) 2002, Vlad Roubtsov  */ public final class SoftValueMap implements Map {     // public: ................................................................     // TODO: for caching, does clearing of entries make sense? only to save     // entry memory -- which does not make sense if the set of key values is not     // growing over time... on the other hand, if the key set is unbounded,     // entry clearing is needed so that the hash table does not get polluted with     // empty-valued entries      // TODO: provide mode that disables entry clearing      // TODO: add shrinking rehashes (is it worth it?)     /**      * Equivalent to <CODE>SoftValueMap(1, 1)</CODE>.      */     public SoftValueMap ()     {         this (1, 1);     }          /**      * Equivalent to <CODE>SoftValueMap(11, 0.75F, getClearCheckFrequency, putClearCheckFrequency)</CODE>.      */     public SoftValueMap (final int readClearCheckFrequency, final int writeClearCheckFrequency)     {         this (11, 0.75F, readClearCheckFrequency, writeClearCheckFrequency);     }          /**      * Constructs a SoftValueMap with specified initial capacity, load factor,      * and cleared value removal frequencies.      *      * @param initialCapacity initial number of hash buckets in the table      * [may not be negative, 0 is equivalent to 1].      * @param loadFactor the load factor to use to determine rehashing points      * [must be in (0.0, 1.0] range].      * @param readClearCheckFrequency specifies that every readClearCheckFrequency      * {@link #get} should check for and remove all mappings whose soft values      * have been cleared by the garbage collector [may not be less than 1].      * @param writeClearCheckFrequency specifies that every writeClearCheckFrequency      * {@link #put} should check for and remove all mappings whose soft values      * have been cleared by the garbage collector [may not be less than 1].      */     public SoftValueMap (int initialCapacity, final float loadFactor, final int readClearCheckFrequency, final int writeClearCheckFrequency)     {         if (initialCapacity < 0)             throw new IllegalArgumentException ("negative input: initialCapacity [" + initialCapacity + "]");         if ((loadFactor <= 0.0) || (loadFactor >= 1.0 + 1.0E-6))             throw new IllegalArgumentException ("loadFactor not in (0.0, 1.0] range: " + loadFactor);         if (readClearCheckFrequency < 1)             throw new IllegalArgumentException ("readClearCheckFrequency not in [1, +inf) range: " + readClearCheckFrequency);         if (writeClearCheckFrequency < 1)             throw new IllegalArgumentException ("writeClearCheckFrequency not in [1, +inf) range: " + writeClearCheckFrequency);                  if (initialCapacity == 0) initialCapacity = 1;                  m_valueReferenceQueue = new ReferenceQueue ();                  m_loadFactor = loadFactor;         m_sizeThreshold = (int) (initialCapacity * loadFactor);                  m_readClearCheckFrequency = readClearCheckFrequency;         m_writeClearCheckFrequency = writeClearCheckFrequency;                  m_buckets = new SoftEntry [initialCapacity];     }               // unsupported operations:              public boolean equals (final Object rhs)     {         throw new UnsupportedOperationException ("not implemented: equals");     }          public int hashCode ()     {         throw new UnsupportedOperationException ("not implemented: hashCode");     }               /**      * Overrides Object.toString() for debug purposes.      */     public String toString ()     {         final StringBuffer s = new StringBuffer ();         debugDump (s);                  return s.toString ();     }               /**      * Returns the number of key-value mappings in this map. Some of the values      * may have been cleared already but not removed from the table.<P>      *      * <B>NOTE:</B> in contrast with the java.util.WeakHashMap implementation,      * this is a constant time operation.      */     public int size ()     {         return m_size;     }          /**      * Returns 'false' is this map contains key-value mappings (even if some of      * the values may have been cleared already but not removed from the table).<P>      *      * <B>NOTE:</B> in contrast with the java.util.WeakHashMap implementation,      * this is a constant time operation.      */     public boolean isEmpty ()     {         return m_size == 0;     }          /**      * Returns the value that is mapped to a given 'key'. Returns      * null if (a) this key has never been mapped or (b) a previously mapped      * value has been cleared by the garbage collector and removed from the table.      *      * @param key mapping key [may not be null].      *      * @return Object value mapping for 'key' [can be null].      */     public Object get (final Object key)     {         if (key == null) throw new IllegalArgumentException ("null input: key");                  if ((++ m_readAccessCount % m_readClearCheckFrequency) == 0) removeClearedValues ();                  // index into the corresponding hash bucket:         final int keyHashCode = key.hashCode ();         final SoftEntry [] buckets = m_buckets;         final int bucketIndex = (keyHashCode & 0x7FFFFFFF) % buckets.length;                  Object result = null;                   // traverse the singly-linked list of entries in the bucket:         for (SoftEntry entry = buckets [bucketIndex]; entry != null; entry = entry.m_next)         {             final Object entryKey = entry.m_key;             if (IDENTITY_OPTIMIZATION)             {                 // note: this uses an early identity comparison opimization, making this a bit                 // faster for table keys that do not override equals() [Thread, etc]                 if ((key == entryKey) || ((keyHashCode == entryKey.hashCode ()) && key.equals (entryKey)))                 {                     final Reference ref = entry.m_softValue;                     result = ref.get (); // may return null to the caller                                          // [see comment for ENQUEUE_FOUND_CLEARED_ENTRIES]                     if (ENQUEUE_FOUND_CLEARED_ENTRIES && (result == null))                     {                         ref.enqueue ();                     }                                          return result;                 }             }             else             {                 if ((keyHashCode == entryKey.hashCode ()) && key.equals (entryKey))                 {                     final Reference ref = entry.m_softValue;                     result = ref.get (); // may return null to the caller                                          // [see comment for ENQUEUE_FOUND_CLEARED_ENTRIES]                     if (ENQUEUE_FOUND_CLEARED_ENTRIES && (result == null))                     {                         ref.enqueue ();                     }                                          return result;                 }             }         }                  return null;     }          /**      * Updates the table to map 'key' to 'value'. Any existing mapping is overwritten.      *      * @param key mapping key [may not be null].      * @param value mapping value [may not be null].      *      * @return Object previous value mapping for 'key' [null if no previous mapping      * existed or its value has been cleared by the garbage collector and removed from the table].      */     public Object put (final Object key, final Object value)     {         if (key == null) throw new IllegalArgumentException ("null input: key");         if (value == null) throw new IllegalArgumentException ("null input: value");                  if ((++ m_writeAccessCount % m_writeClearCheckFrequency) == 0) removeClearedValues ();                      SoftEntry currentKeyEntry = null;                  // detect if 'key' is already in the table [in which case, set 'currentKeyEntry' to point to its entry]:                  // index into the corresponding hash bucket:         final int keyHashCode = key.hashCode ();         SoftEntry [] buckets = m_buckets;         int bucketIndex = (keyHashCode & 0x7FFFFFFF) % buckets.length;                  // traverse the singly-linked list of entries in the bucket:         for (SoftEntry entry = buckets [bucketIndex]; entry != null; entry = entry.m_next)         {             final Object entryKey = entry.m_key;                          if (IDENTITY_OPTIMIZATION)             {                 // note: this uses an early identity comparison opimization, making this a bit                 // faster for table keys that do not override equals() [Thread, etc]                 if ((key == entryKey) || ((keyHashCode == entryKey.hashCode ()) && key.equals (entryKey)))                 {                     currentKeyEntry = entry;                     break;                 }             }             else             {                 if ((keyHashCode == entryKey.hashCode ()) && key.equals (entryKey))                 {                     currentKeyEntry = entry;                     break;                 }             }         }                  if (currentKeyEntry != null)         {             // replace the current value:                          final IndexedSoftReference ref = currentKeyEntry.m_softValue;             final Object currentKeyValue = ref.get (); // can be null already [no need to work around the get() bug, though]                          if (currentKeyValue == null) ref.m_bucketIndex = -1; // disable removal by removeClearedValues() [need to do this because of the identity comparison there]             currentKeyEntry.m_softValue = new IndexedSoftReference (value, m_valueReferenceQueue, bucketIndex);                          return currentKeyValue; // may return null to the caller         }         else         {             // add a new entry:                          if (m_size >= m_sizeThreshold) rehash ();                          // recompute the hash bucket index:             buckets = m_buckets;             bucketIndex = (keyHashCode & 0x7FFFFFFF) % buckets.length;             final SoftEntry bucketListHead = buckets [bucketIndex];             final SoftEntry newEntry = new SoftEntry (m_valueReferenceQueue, key, value, bucketListHead, bucketIndex);             buckets [bucketIndex] = newEntry;                          ++ m_size;                          return null;         }     }          public Object remove (final Object key)     {         if (key == null) throw new IllegalArgumentException ("null input: key");                  if ((++ m_writeAccessCount % m_writeClearCheckFrequency) == 0) removeClearedValues ();         // index into the corresponding hash bucket:         final int keyHashCode = key.hashCode ();         final SoftEntry [] buckets = m_buckets;         final int bucketIndex = (keyHashCode & 0x7FFFFFFF) % buckets.length;                  Object result = null;         // traverse the singly-linked list of entries in the bucket:         for (SoftEntry entry = buckets [bucketIndex], prev = null; entry != null; prev = entry, entry = entry.m_next)         {             final Object entryKey = entry.m_key;                          if ((IDENTITY_OPTIMIZATION && (entryKey == key)) || ((keyHashCode == entryKey.hashCode ()) && key.equals (entryKey)))             {                 if (prev == null) // head of the list                 {                     buckets [bucketIndex] = entry.m_next;                 }                 else                 {                     prev.m_next = entry.m_next;                 }                                  final IndexedSoftReference ref = entry.m_softValue;                  result = ref.get (); // can be null already [however, no need to work around 4485942]                                  // [regardless of whether the value has been enqueued or not, disable its processing by removeClearedValues() since the entire entry is removed here]                 ref.m_bucketIndex = -1;                                  // help GC:                 entry.m_softValue = null;                 entry.m_key = null;                 entry.m_next = null;                 entry = null;                              -- m_size;                 break;             }         }                  return result;     }          public void clear ()     {         final SoftEntry [] buckets = m_buckets;                  for (int b = 0, bLimit = buckets.length; b < bLimit; ++ b)         {             for (SoftEntry entry = buckets [b]; entry != null; )             {                 final SoftEntry next = entry.m_next; // remember next pointer because we are going to reuse this entry                 // [regardless of whether the value has been enqueued or not, disable its processing by removeClearedValues()]                 entry.m_softValue.m_bucketIndex = -1;                                  // help GC:                 entry.m_softValue = null;                 entry.m_next = null;                 entry.m_key = null;                                  entry = next;             }                          buckets [b] = null;         }                  m_size = 0;         m_readAccessCount = 0;         m_writeAccessCount = 0;     }     // unsupported operations:          public boolean containsKey (final Object key)     {         throw new UnsupportedOperationException ("not implemented: containsKey");     }          public boolean containsValue (final Object value)     {         throw new UnsupportedOperationException ("not implemented: containsValue");     }              public void putAll (final Map map)     {         throw new UnsupportedOperationException ("not implemented: putAll");     }          public Set keySet ()     {         throw new UnsupportedOperationException ("not implemented: keySet");     }          public Set entrySet ()     {         throw new UnsupportedOperationException ("not implemented: entrySet");     }     public Collection values ()     {         throw new UnsupportedOperationException ("not implemented: values");     }          // protected: .............................................................     // package: ...............................................................               void debugDump (final StringBuffer out)     {         if (out != null)         {             out.append (getClass ().getName ().concat ("@").concat (Integer.toHexString (System.identityHashCode (this)))); out.append (EOL);             out.append ("size = " + m_size + ", bucket table size = " + m_buckets.length + ", load factor = " + m_loadFactor + EOL);             out.append ("size threshold = " + m_sizeThreshold + ", get clear frequency = " + m_readClearCheckFrequency + ", put clear frequency = " + m_writeClearCheckFrequency + EOL);             out.append ("get count: " + m_readAccessCount + ", put count: " + m_writeAccessCount + EOL);         }     }     // private: ...............................................................     /**      * An extension of WeakReference that can store an index of the bucket it      * is associated with.      */     static class IndexedSoftReference extends SoftReference     {         IndexedSoftReference (final Object referent, ReferenceQueue queue, final int bucketIndex)         {             super (referent, queue);                          m_bucketIndex = bucketIndex;         }                  int m_bucketIndex;              } // end of nested class               /**      * The structure used for chaining colliding keys.      */     static class SoftEntry     {         SoftEntry (final ReferenceQueue valueReferenceQueue, final Object key, Object value, final SoftEntry next, final int bucketIndex)         {             m_key = key;                          m_softValue = new IndexedSoftReference (value, valueReferenceQueue, bucketIndex); // ... do not retain a strong reference to the value             value = null;                          m_next = next;         }                  IndexedSoftReference m_softValue; // soft reference to the value [never null]         Object m_key;  // strong reference to the key [never null]                  SoftEntry m_next; // singly-linked list link              } // end of nested class          /**      * Re-hashes the table into a new array of buckets. During the process      * cleared value entries are discarded, making for another efficient cleared      * value removal method.      */     private void rehash ()     {         // TODO: it is possible to run this method twice, first time using the 2*k+1 prime sequencer for newBucketCount         // and then with that value reduced to actually shrink capacity. As it is right now, the bucket table can         // only grow in size                  final SoftEntry [] buckets = m_buckets;                  final int newBucketCount = (m_buckets.length << 1) + 1;         final SoftEntry [] newBuckets = new SoftEntry [newBucketCount];                  int newSize = 0;                  // rehash all entry chains in every bucket:         for (int b = 0, bLimit = buckets.length; b < bLimit; ++ b)         {             for (SoftEntry entry = buckets [b]; entry != null; )             {                 final SoftEntry next = entry.m_next; // remember next pointer because we are going to reuse this entry                                  IndexedSoftReference ref = entry.m_softValue; // get the soft value reference                                                  Object entryValue = ref.get (); // convert the soft reference to a local strong one                              // skip entries whose keys have been cleared: this also saves on future removeClearedValues() work                 if (entryValue != null)                 {                     // [assertion: 'softValue' couldn't have been enqueued already and can't be enqueued until strong reference in 'entryKey' is nulled out]                                          // index into the corresponding new hash bucket:                     final int entryKeyHashCode = entry.m_key.hashCode ();                     final int newBucketIndex = (entryKeyHashCode & 0x7FFFFFFF) % newBucketCount;                                          final SoftEntry bucketListHead = newBuckets [newBucketIndex];                     entry.m_next = bucketListHead;                     newBuckets [newBucketIndex] = entry;                                          // adjust bucket index:                     ref.m_bucketIndex = newBucketIndex;                                  ++ newSize;                                          entryValue = null;                 }                 else                 {                     // ['softValue' may or may not have been enqueued already]                                          // adjust bucket index:                     // [regardless of whether 'softValue' has been enqueued or not, disable its removal by removeClearedValues() since the buckets get restructured]                     ref.m_bucketIndex = -1;                 }                                  entry = next;             }         }                  if (DEBUG)         {             if (m_size > newSize) System.out.println ("DEBUG: rehash() cleared " + (m_size - newSize) + " values, new size = " + newSize);         }                  m_size = newSize;         m_sizeThreshold = (int) (newBucketCount * m_loadFactor);         m_buckets = newBuckets;     }          /**      * Removes all entries whose soft values have been cleared _and_ enqueued.      * See comments below for why this is safe wrt to rehash().      */     private void removeClearedValues ()     {         int count = 0;          next:   for (Reference _ref; (_ref = m_valueReferenceQueue.poll ()) != null; )         {             // remove entry containing '_ref' using its bucket index and identity comparison:                          // index into the corresponding hash bucket:             final int bucketIndex = ((IndexedSoftReference) _ref).m_bucketIndex;                          if (bucketIndex >= 0) // skip keys that were already removed by rehash()             {                 // [assertion: this reference was not cleared when the last rehash() ran and so its m_bucketIndex is correct]                                  // traverse the singly-linked list of entries in the bucket:                 for (SoftEntry entry = m_buckets [bucketIndex], prev = null; entry != null; prev = entry, entry = entry.m_next)                 {                     if (entry.m_softValue == _ref)                     {                         if (prev == null) // head of the list                         {                             m_buckets [bucketIndex] = entry.m_next;                         }                         else                         {                             prev.m_next = entry.m_next;                         }                                              entry.m_softValue = null;                         entry.m_key = null;                         entry.m_next = null;                         entry = null;                                              -- m_size;                                                  if (DEBUG) ++ count;                         continue next;                     }                 }                                  // no match found this can happen if a soft value got replaced by a put                                  final StringBuffer msg = new StringBuffer ("removeClearedValues(): soft reference [" + _ref + "] did not match within bucket #" + bucketIndex + EOL);                 debugDump (msg);                              throw new Error (msg.toString ());             }             // else: it has already been removed by rehash() or other methods         }                  if (DEBUG)         {             if (count > 0) System.out.println ("DEBUG: removeClearedValues() cleared " + count + " keys, new size = " + m_size);         }     }               private final ReferenceQueue m_valueReferenceQueue; // reference queue for all references used by SoftEntry objects used by this table     private final float m_loadFactor; // determines the setting of m_sizeThreshold     private final int m_readClearCheckFrequency, m_writeClearCheckFrequency; // parameters determining frequency of running removeClearedKeys() by get() and put()/remove(), respectively          private SoftEntry [] m_buckets; // table of buckets     private int m_size; // number of values in the table, not cleared as of last check     private int m_sizeThreshold; // size threshold for rehashing     private int m_readAccessCount, m_writeAccessCount;          private static final String EOL = System.getProperty ("line.separator", "\n");          private static final boolean IDENTITY_OPTIMIZATION          = true;          // setting this to 'true' is an optimization and a workaround for bug 4485942:     private static final boolean ENQUEUE_FOUND_CLEARED_ENTRIES  = true;           private static final boolean DEBUG = false; } // end of class // ----------------------------------------------------------------------------