From 869725582cf892d11d2903dd177dcd4df4ccd94d Mon Sep 17 00:00:00 2001 From: pjack Date: Thu, 15 Aug 2002 03:22:29 +0000 Subject: [PATCH] 1. Collection views are now backed by map. 2. Used bit-mixing hash function. 3. Added docs about the non-atomic nature of bulk operations. 4. Improved performance of size() operation. 5. Added atomic(Runnable) method. git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/collections/trunk@130773 13f79535-47bb-0310-9956-ffa450edef68 --- .../commons/collections/StaticBucketMap.java | 403 ++++++++++++++---- 1 file changed, 314 insertions(+), 89 deletions(-) diff --git a/src/java/org/apache/commons/collections/StaticBucketMap.java b/src/java/org/apache/commons/collections/StaticBucketMap.java index f10c042e6..9aa19a624 100644 --- a/src/java/org/apache/commons/collections/StaticBucketMap.java +++ b/src/java/org/apache/commons/collections/StaticBucketMap.java @@ -1,7 +1,7 @@ /* - * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/StaticBucketMap.java,v 1.2 2002/07/10 03:35:32 mas Exp $ - * $Revision: 1.2 $ - * $Date: 2002/07/10 03:35:32 $ + * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//collections/src/java/org/apache/commons/collections/StaticBucketMap.java,v 1.3 2002/08/15 03:22:29 pjack Exp $ + * $Revision: 1.3 $ + * $Date: 2002/08/15 03:22:29 $ * * ==================================================================== * @@ -60,36 +60,91 @@ */ package org.apache.commons.collections; +import java.util.AbstractCollection; +import java.util.AbstractSet; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.ArrayList; +import java.util.NoSuchElementException; /** * A StaticBucketMap is an efficient, thread-safe implementation of * java.util.Map that performs well in in a highly * thread-contentious environment. The map supports very efficient - * get, put, contains, and - * remove operations, assuming (approximate) uniform hashing and - * that the number of entries does not exceed the size of the map. If the - * number of entries exceeds the size of the map or if the hashcodes of the + * {@link #get(Object) get}, {@link #put(Object,Object) put}, + * {@link #remove(Object) remove} and {@link #containsKey(Object) containsKey} + * operations, assuming (approximate) uniform hashing and + * that the number of entries does not exceed the number of buckets. If the + * number of entries exceeds the number of buckets or if the hashcodes of the * objects are not uniformly distributed, these operations have a worst case * scenario that is proportional to the number of elements in the map - * (O(n)). + * (O(n)).

+ * + * Each bucket in the hash table has its own monitor, so two threads can + * safely operate on the map at the same time, often without incurring any + * monitor contention. This means that you don't have to wrap instances + * of this class with {@link java.util.Collections#synchronizedMap(Map)}; + * instances are already thread-safe. Unfortunately, however, this means + * that this map implementation behaves in ways you may find disconcerting. + * Bulk operations, such as {@link #putAll(Map) putAll} or the + * {@link Collection#retainAll(Collection) retainAll} operation in collection + * views, are not atomic. If two threads are simultaneously + * executing + * + *

+ *   staticBucketMapInstance.putAll(map);
+ * 
+ * + * and + * + *
+ *   staticBucketMapInstance.entrySet().removeAll(map.entrySet());
+ * 
+ * + * then the results are generally random. Those two statement could cancel + * each other out, leaving staticBucketMapInstance essentially + * unchanged, or they could leave some random subset of map in + * staticBucketMapInstance.

+ * + * Also, much like an encyclopedia, the results of {@link #size()} and + * {@link #isEmpty()} are out-of-date as soon as they are produced.

+ * + * The iterators returned by the collection views of this class are not + * fail-fast. They will never raise a + * {@link java.util.ConcurrentModificationException}. Keys and values + * added to the map after the iterator is created do not necessarily appear + * during iteration. Similarly, the iterator does not necessarily fail to + * return keys and values that were removed after the iterator was created.

+ * + * Finally, unlike {@link java.util.HashMap}-style implementations, this + * class never rehashes the map. The number of buckets is fixed + * at construction time and never altered. Performance may degrade if + * you do not allocate enough buckets upfront.

+ * + * The {@link #atomic(Runnable)} method is provided to allow atomic iterations + * and bulk operations; however, overuse of {@link #atomic(Runnable) atomic} + * will basically result in a map that's slower than an ordinary synchronized + * {@link java.util.HashMap}. + * + * Use this class if you do not require reliable bulk operations and + * iterations, or if you can make your own guarantees about how bulk + * operations will affect the map.

* * @author Berin Loritsch * @author Gerhard Froehlich * @author Michael A. Smith - * @version CVS $Revision: 1.2 $ $Date: 2002/07/10 03:35:32 $ + * @author Paul Jack + * @version CVS $Revision: 1.3 $ $Date: 2002/08/15 03:22:29 $ * @since Avalon 4.0 */ public final class StaticBucketMap implements Map { private static final int DEFAULT_BUCKETS = 255; - private final Node[] m_buckets; - private final Object[] m_locks; + private Node[] m_buckets; + private Lock[] m_locks; /** * Initializes the map with the default number of buckets (255). @@ -106,6 +161,8 @@ public final class StaticBucketMap implements Map * chances for thread contention. The fewer buckets, the more chances for * thread contention. The more buckets the fewer chances for thread * contention. + * + * @param numBuckets the number of buckets for this map */ public StaticBucketMap( int numBuckets ) { @@ -118,11 +175,11 @@ public final class StaticBucketMap implements Map } m_buckets = new Node[ size ]; - m_locks = new Object[ size ]; + m_locks = new Lock[ size ]; for( int i = 0; i < size; i++ ) { - m_locks[ i ] = new Object(); + m_locks[ i ] = new Lock(); } } @@ -142,34 +199,23 @@ public final class StaticBucketMap implements Map private final int getHash( Object key ) { if( key == null ) return 0; - final int hash = key.hashCode() % m_buckets.length; + int hash = key.hashCode(); + hash += ~(hash << 15); + hash ^= (hash >>> 10); + hash += (hash << 3); + hash ^= (hash >>> 6); + hash += ~(hash << 11); + hash ^= (hash >>> 16); + hash %= m_buckets.length; return ( hash < 0 ) ? hash * -1 : hash; } /** - * Obtain a Set for the keys. This operation crosses bucket boundaries, - * so it is less efficient, and greatly increases the chance for thread - * contention. + * Returns a set view of this map's keys. */ public Set keySet() { - Set keySet = new HashSet(); - - for( int i = 0; i < m_buckets.length; i++ ) - { - synchronized( m_locks[ i ] ) - { - Node n = m_buckets[ i ]; - - while( n != null ) - { - keySet.add( n.key ); - n = n.next; - } - } - } - - return keySet; + return new KeySet(); } /** @@ -181,16 +227,7 @@ public final class StaticBucketMap implements Map for( int i = 0; i < m_buckets.length; i++ ) { - synchronized( m_locks[ i ] ) - { - Node n = m_buckets[ i ]; - - while( n != null ) - { - cnt++; - n = n.next; - } - } + cnt += m_locks[i].size; } return cnt; @@ -213,6 +250,7 @@ public final class StaticBucketMap implements Map n.key = key; n.value = value; m_buckets[ hash ] = n; + m_locks[hash].size++; return null; } @@ -237,6 +275,7 @@ public final class StaticBucketMap implements Map newNode.key = key; newNode.value = value; n.next = newNode; + m_locks[hash].size++; } return null; @@ -328,23 +367,7 @@ public final class StaticBucketMap implements Map */ public Collection values() { - ArrayList values = new ArrayList(); - - for( int i = 0; i < m_buckets.length; i++ ) - { - synchronized( m_locks[ i ] ) - { - Node n = m_buckets[ i ]; - - while( n != null ) - { - values.add( n.value ); - n = n.next; - } - } - } - - return values; + return new Values(); } /** @@ -354,23 +377,7 @@ public final class StaticBucketMap implements Map */ public Set entrySet() { - Set entrySet = new HashSet(); - - for( int i = 0; i < m_buckets.length; i++ ) - { - synchronized( m_locks[ i ] ) - { - Node n = m_buckets[ i ]; - - while( n != null ) - { - entrySet.add( n ); - n = n.next; - } - } - } - - return entrySet; + return new EntrySet(); } /** @@ -414,7 +421,7 @@ public final class StaticBucketMap implements Map // Set the next node of the previous node to be the node after this one. prev.next = n.next; } - + m_locks[hash].size--; return n.value; } @@ -431,15 +438,7 @@ public final class StaticBucketMap implements Map */ public final boolean isEmpty() { - for( int i = 0; i < m_buckets.length; i++ ) - { - if( m_buckets[ i ] != null ) - { - return false; - } - } - - return true; + return size() == 0; } /** @@ -449,10 +448,21 @@ public final class StaticBucketMap implements Map { for( int i = 0; i < m_buckets.length; i++ ) { - m_buckets[ i ] = null; + Lock lock = m_locks[i]; + synchronized (lock) { + m_buckets[ i ] = null; + lock.size = 0; + } } } + /** + * Returns true if the given object is a map with the same mappings + * as this map. + * + * @return true if the given object is the a map with same mappings + * as this map + */ public final boolean equals( Object obj ) { if( obj == null ) return false; @@ -465,6 +475,11 @@ public final class StaticBucketMap implements Map return entrySet().equals(other.entrySet()); } + /** + * Returns a hash code for this map. + * + * @return a hash code for this map + */ public final int hashCode() { int hashCode = 0; @@ -532,4 +547,214 @@ public final class StaticBucketMap implements Map return retVal; } } + + private final static class Lock { + + public int size; + + } + + + private class EntryIterator implements Iterator { + + private ArrayList current = new ArrayList(); + private int bucket; + private Map.Entry last; + + + public boolean hasNext() { + if (current.size() > 0) return true; + while (bucket < m_buckets.length) { + synchronized (m_locks[bucket]) { + Node n = m_buckets[bucket]; + while (n != null) { + current.add(n); + n = n.next; + } + bucket++; + if (current.size() > 0) return true; + } + } + return false; + } + + protected Map.Entry nextEntry() { + if (!hasNext()) throw new NoSuchElementException(); + last = (Map.Entry)current.remove(current.size() - 1); + return last; + } + + public Object next() { + return nextEntry(); + } + + public void remove() { + if (last == null) throw new IllegalStateException(); + StaticBucketMap.this.remove(last.getKey()); + last = null; + } + + } + + private class ValueIterator extends EntryIterator { + + public Object next() { + return nextEntry().getValue(); + } + + } + + private class KeyIterator extends EntryIterator { + + public Object next() { + return nextEntry().getKey(); + } + + } + + private class EntrySet extends AbstractSet { + + public int size() { + return StaticBucketMap.this.size(); + } + + public void clear() { + StaticBucketMap.this.clear(); + } + + public Iterator iterator() { + return new EntryIterator(); + } + + public boolean contains(Object o) { + Map.Entry entry = (Map.Entry)o; + int hash = getHash(entry.getKey()); + synchronized (m_locks[hash]) { + for (Node n = m_buckets[hash]; n != null; n = n.next) { + if (n.equals(entry)) return true; + } + } + return false; + } + + public boolean remove(Object o) { + Map.Entry entry = (Map.Entry)o; + int hash = getHash(entry.getKey()); + synchronized (m_locks[hash]) { + for (Node n = m_buckets[hash]; n != null; n = n.next) { + if (n.equals(entry)) { + StaticBucketMap.this.remove(n.getKey()); + return true; + } + } + } + return false; + } + + } + + + private class KeySet extends AbstractSet { + + public int size() { + return StaticBucketMap.this.size(); + } + + public void clear() { + StaticBucketMap.this.clear(); + } + + public Iterator iterator() { + return new KeyIterator(); + } + + public boolean contains(Object o) { + return StaticBucketMap.this.containsKey(o); + } + + public boolean remove(Object o) { + int hash = getHash(o); + synchronized (m_locks[hash]) { + for (Node n = m_buckets[hash]; n != null; n = n.next) { + Object k = n.getKey(); + if ((k == o) || ((k != null) && k.equals(o))) { + StaticBucketMap.this.remove(k); + return true; + } + } + } + return false; + + } + + } + + + private class Values extends AbstractCollection { + + public int size() { + return StaticBucketMap.this.size(); + } + + public void clear() { + StaticBucketMap.this.clear(); + } + + public Iterator iterator() { + return new ValueIterator(); + } + + } + + + /** + * Prevents any operations from occuring on this map while the + * given {@link Runnable} executes. This method can be used, for + * instance, to execute a bulk operation atomicly: + * + *

+     *    staticBucketMapInstance.atomic(new Runnable() {
+     *        public void run() {
+     *            staticBucketMapInstance.putAll(map);
+     *        }
+     *    });
+     *  
+ * + * It can also be used if you need a reliable iterator: + * + *
+     *    staticBucketMapInstance.atomic(new Runnable() {
+     *        public void run() {
+     *            Iterator iterator = staticBucketMapInstance.iterator();
+     *            while (iterator.hasNext()) {
+     *                foo(iterator.next();
+     *            }
+     *        }
+     *    });
+     *  
+ * + * Implementation note: This method requires a lot of time + * and a ton of stack space. Essentially a recursive algorithm is used + * to enter each bucket's monitor. If you have twenty thousand buckets + * in your map, then the recursive method will be invoked twenty thousand + * times. You have been warned. + * + * @param r the code to execute atomicly + */ + public void atomic(Runnable r) { + if (r == null) throw new NullPointerException(); + atomic(r, 0); + } + + private void atomic(Runnable r, int bucket) { + if (bucket >= m_buckets.length) { + r.run(); + return; + } + synchronized (m_locks[bucket]) { + atomic(r, bucket + 1); + } + } + + }