diff --git a/core/java/src/net/i2p/util/DecayingBloomFilter.java b/core/java/src/net/i2p/util/DecayingBloomFilter.java
index 73e4f6523f0949028f4f89e14b02337c764b1169..862b33d5e3e2b98bd8830610b391e1e6ab9207e7 100644
--- a/core/java/src/net/i2p/util/DecayingBloomFilter.java
+++ b/core/java/src/net/i2p/util/DecayingBloomFilter.java
@@ -1,6 +1,8 @@
 package net.i2p.util;
 
 import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
@@ -9,15 +11,17 @@ import org.xlattice.crypto.filters.BloomSHA1;
 
 /**
  * Series of bloom filters which decay over time, allowing their continual use
- * for time sensitive data.  This has a fixed size (currently 1MB per decay 
+ * for time sensitive data.  This has a fixed size (per
  * period, using two periods overall), allowing this to pump through hundreds of
  * entries per second with virtually no false positive rate.  Down the line, 
  * this may be refactored to allow tighter control of the size necessary for the
- * contained bloom filters, but a fixed 2MB overhead isn't that bad.
+ * contained bloom filters.
  *
- * NOTE: At 1MBps, the tunnel IVV will see an unacceptable false positive rate
- * of almost 0.1% with the current m and k values; however using DHS instead will use 30MB.
- * Further analysis and tweaking for the tunnel IVV may be required.
+ * See main() for an analysis of false positive rate.
+ * See BloomFilterIVValidator for instantiation parameters.
+ * See DecayingHashSet for a smaller and simpler version.
+ * @see net.i2p.router.tunnel.BloomFilterIVValidator
+ * @see net.i2p.util.DecayingHashSet
  */
 public class DecayingBloomFilter {
     protected final I2PAppContext _context;
@@ -26,18 +30,21 @@ public class DecayingBloomFilter {
     private BloomSHA1 _previous;
     protected final int _durationMs;
     protected final int _entryBytes;
-    private byte _extenders[][];
-    private byte _extended[];
-    private byte _longToEntry[];
-    private long _longToEntryMask;
+    private final byte _extenders[][];
+    private final byte _extended[];
+    private final byte _longToEntry[];
+    private final long _longToEntryMask;
     protected long _currentDuplicates;
     protected volatile boolean _keepDecaying;
-    protected SimpleTimer.TimedEvent _decayEvent;
+    protected final SimpleTimer.TimedEvent _decayEvent;
     /** just for logging */
     protected final String _name;
+    /** synchronize against this lock when switching double buffers */
+    protected final ReentrantReadWriteLock _reorganizeLock = new ReentrantReadWriteLock();
     
     private static final int DEFAULT_M = 23;
     private static final int DEFAULT_K = 11;
+    /** true for debugging */
     private static final boolean ALWAYS_MISS = false;
    
     /** only for extension by DHS */
@@ -47,6 +54,15 @@ public class DecayingBloomFilter {
         _entryBytes = entryBytes;
         _name = name;
         _durationMs = durationMs;
+        // all final
+        _extenders = null;
+        _extended = null;
+        _longToEntry = null;
+        _longToEntryMask = 0;
+        context.addShutdownTask(new Shutdown());
+        _decayEvent = new DecayEvent();
+        _keepDecaying = true;
+        SimpleTimer.getInstance().addEvent(_decayEvent, _durationMs);
     }
 
     /**
@@ -92,6 +108,11 @@ public class DecayingBloomFilter {
             _extended = new byte[32];
             _longToEntry = new byte[_entryBytes];
             _longToEntryMask = (1l << (_entryBytes * 8l)) -1;
+        } else {
+            // final
+            _extended = null;
+            _longToEntry = null;
+            _longToEntryMask = 0;
         }
         _decayEvent = new DecayEvent();
         _keepDecaying = true;
@@ -101,12 +122,12 @@ public class DecayingBloomFilter {
                      " numExtenders = " + numExtenders + " cycle (s) = " + (durationMs / 1000));
         // try to get a handle on memory usage vs. false positives
         context.statManager().createRateStat("router.decayingBloomFilter." + name + ".size",
-             "Size", "Router", new long[] { Math.max(60*1000, durationMs) });
+             "Size", "Router", new long[] { 10 * Math.max(60*1000, durationMs) });
         context.statManager().createRateStat("router.decayingBloomFilter." + name + ".dups",
-             "1000000 * Duplicates/Size", "Router", new long[] { Math.max(60*1000, durationMs) });
+             "1000000 * Duplicates/Size", "Router", new long[] { 10 * Math.max(60*1000, durationMs) });
         context.statManager().createRateStat("router.decayingBloomFilter." + name + ".log10(falsePos)",
              "log10 of the false positive rate (must have net.i2p.util.DecayingBloomFilter=DEBUG)",
-             "Router", new long[] { Math.max(60*1000, durationMs) });
+             "Router", new long[] { 10 * Math.max(60*1000, durationMs) });
         context.addShutdownTask(new Shutdown());
     }
     
@@ -121,16 +142,14 @@ public class DecayingBloomFilter {
 
     public long getCurrentDuplicateCount() { return _currentDuplicates; }
 
+    /** unsynchronized but only used for logging elsewhere */
     public int getInsertedCount() { 
-        synchronized (this) {
             return _current.size() + _previous.size(); 
-        }
     }
 
+    /** unshyncronized, only used for logging elsewhere */
     public double getFalsePositiveRate() { 
-        synchronized (this) {
             return _current.falsePositives(); 
-        }
     }
     
     /** 
@@ -150,9 +169,10 @@ public class DecayingBloomFilter {
         if (len != _entryBytes) 
             throw new IllegalArgumentException("Bad entry [" + len + ", expected " 
                                                + _entryBytes + "]");
-        synchronized (this) {
+        getReadLock();
+        try {
             return locked_add(entry, off, len, true);
-        }
+        } finally { releaseReadLock(); }
     }
     
     /** 
@@ -172,9 +192,10 @@ public class DecayingBloomFilter {
         } else {
             DataHelper.toLong(_longToEntry, 0, _entryBytes, entry);
         }
-        synchronized (this) {
+        getReadLock();
+        try {
             return locked_add(_longToEntry, 0, _longToEntry.length, true);
-        }
+        } finally { releaseReadLock(); }
     }
     
     /** 
@@ -192,9 +213,10 @@ public class DecayingBloomFilter {
         } else {
             DataHelper.toLong(_longToEntry, 0, _entryBytes, entry);
         }
-        synchronized (this) {
+        getReadLock();
+        try {
             return locked_add(_longToEntry, 0, _longToEntry.length, false);
-        }
+        } finally { releaseReadLock(); }
     }
     
     private boolean locked_add(byte entry[], int offset, int len, boolean addIfNew) {
@@ -204,38 +226,48 @@ public class DecayingBloomFilter {
             for (int i = 0; i < _extenders.length; i++)
                 DataHelper.xor(entry, offset, _extenders[i], 0, _extended, _entryBytes * (i+1), _entryBytes);
 
-            boolean seen = _current.locked_member(_extended);
-            seen = seen || _previous.locked_member(_extended);
+            BloomSHA1.FilterKey key = _current.getFilterKey(_extended, 0, 32);
+            boolean seen = _current.locked_member(key);
+            if (!seen)
+                seen = _previous.locked_member(key);
             if (seen) {
                 _currentDuplicates++;
+                _current.release(key);
                 return true;
             } else {
                 if (addIfNew) {
-                    _current.locked_insert(_extended);
+                    _current.locked_insert(key);
                 }
+                _current.release(key);
                 return false;
             }
         } else {
-            boolean seen = _current.locked_member(entry, offset, len);
-            seen = seen || _previous.locked_member(entry, offset, len);
+            BloomSHA1.FilterKey key = _current.getFilterKey(entry, offset, len);
+            boolean seen = _current.locked_member(key);
+            if (!seen)
+                seen = _previous.locked_member(key);
             if (seen) {
                 _currentDuplicates++;
+                _current.release(key);
                 return true;
             } else {
                 if (addIfNew) {
-                    _current.locked_insert(entry, offset, len);
+                    _current.locked_insert(key);
                 }
+                _current.release(key);
                 return false;
             }
         }
     }
     
     public void clear() {
-        synchronized (this) {
+        if (!getWriteLock())
+            return;
+        try {
             _current.clear();
             _previous.clear();
             _currentDuplicates = 0;
-        }
+        } finally { releaseWriteLock(); }
     }
     
     public void stopDecaying() {
@@ -243,11 +275,13 @@ public class DecayingBloomFilter {
         SimpleTimer.getInstance().removeEvent(_decayEvent);
     }
     
-    private void decay() {
+    protected void decay() {
         int currentCount = 0;
         long dups = 0;
         double fpr = 0d;
-        synchronized (this) {
+        if (!getWriteLock())
+            return;
+        try {
             BloomSHA1 tmp = _previous;
             currentCount = _current.size();
             if (_log.shouldLog(Log.DEBUG) && currentCount > 0)
@@ -257,20 +291,20 @@ public class DecayingBloomFilter {
             _current.clear();
             dups = _currentDuplicates;
             _currentDuplicates = 0;
-        }
+        } finally { releaseWriteLock(); }
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Decaying the filter " + _name + " after inserting " + currentCount 
                        + " elements and " + dups + " false positives with FPR = " + fpr);
         _context.statManager().addRateData("router.decayingBloomFilter." + _name + ".size",
-                                           currentCount, 0);
+                                           currentCount);
         if (currentCount > 0)
             _context.statManager().addRateData("router.decayingBloomFilter." + _name + ".dups",
-                                               1000l*1000*dups/currentCount, 0);
+                                               1000l*1000*dups/currentCount);
         if (fpr > 0d) {
             // only if log.shouldLog(Log.DEBUG) ...
             long exponent = (long) Math.log10(fpr);
             _context.statManager().addRateData("router.decayingBloomFilter." + _name + ".log10(falsePos)",
-                                               exponent, 0);
+                                               exponent);
         }
     }
     
@@ -283,12 +317,42 @@ public class DecayingBloomFilter {
         }
     }
     
+    /** @since 0.8.11 moved from DecayingHashSet */
+    protected void getReadLock() {
+        _reorganizeLock.readLock().lock();
+    }
+
+    /** @since 0.8.11 moved from DecayingHashSet */
+    protected void releaseReadLock() {
+        _reorganizeLock.readLock().unlock();
+    }
+
+    /**
+     *  @return true if the lock was acquired
+     *  @since 0.8.11 moved from DecayingHashSet
+     */
+    protected boolean getWriteLock() {
+        try {
+            boolean rv = _reorganizeLock.writeLock().tryLock(5000, TimeUnit.MILLISECONDS);
+            if (!rv)
+                _log.error("no lock, size is: " + _reorganizeLock.getQueueLength(), new Exception("rats"));
+            return rv;
+        } catch (InterruptedException ie) {}
+        return false;
+    }
+
+    /** @since 0.8.11 moved from DecayingHashSet */
+    protected void releaseWriteLock() {
+        _reorganizeLock.writeLock().unlock();
+    }
+
     /**
      *  This filter is used only for participants and OBEPs, not
      *  IBGWs, so depending on your assumptions of avg. tunnel length,
      *  the performance is somewhat better than the gross share BW
      *  would indicate.
      *
+     *<pre>
      *  Following stats for m=23, k=11:
      *  Theoretical false positive rate for   16 KBps: 1.17E-21
      *  Theoretical false positive rate for   24 KBps: 9.81E-20
@@ -302,18 +366,37 @@ public class DecayingBloomFilter {
      *  1280 4.5E-5; 1792 5.6E-4; 2048 0.14%
      *
      *  Following stats for m=25, k=10:
-     *  1792 2.4E-6; 4096 0.14%
+     *  1792 2.4E-6; 4096 0.14%; 5120 0.6%; 6144 1.7%; 8192 6.8%; 10240 15%
+     *</pre>
      */
     public static void main(String args[]) {
+        System.out.println("Usage: DecayingBloomFilter [kbps [m [iterations]]] (default 256 23 10)");
         int kbps = 256;
+        if (args.length >= 1) {
+            try {
+                kbps = Integer.parseInt(args[0]);
+            } catch (NumberFormatException nfe) {}
+        }
+        int m = DEFAULT_M;
+        if (args.length >= 2) {
+            try {
+                m = Integer.parseInt(args[1]);
+            } catch (NumberFormatException nfe) {}
+        }
         int iterations = 10;
-        testByLong(kbps, iterations);
-        testByBytes(kbps, iterations);
+        if (args.length >= 3) {
+            try {
+                iterations = Integer.parseInt(args[2]);
+            } catch (NumberFormatException nfe) {}
+        }
+        testByLong(kbps, m, iterations);
+        testByBytes(kbps, m, iterations);
     }
-    private static void testByLong(int kbps, int numRuns) {
+
+    private static void testByLong(int kbps, int m, int numRuns) {
         int messages = 60 * 10 * kbps;
         Random r = new Random();
-        DecayingBloomFilter filter = new DecayingBloomFilter(I2PAppContext.getGlobalContext(), 600*1000, 8);
+        DecayingBloomFilter filter = new DecayingBloomFilter(I2PAppContext.getGlobalContext(), 600*1000, 8, "test", m);
         int falsePositives = 0;
         long totalTime = 0;
         double fpr = 0d;
@@ -322,7 +405,7 @@ public class DecayingBloomFilter {
             for (int i = 0; i < messages; i++) {
                 if (filter.add(r.nextLong())) {
                     falsePositives++;
-                    System.out.println("False positive " + falsePositives + " (testByLong j=" + j + " i=" + i + ")");
+                    //System.out.println("False positive " + falsePositives + " (testByLong j=" + j + " i=" + i + ")");
                 }
             }
             totalTime += System.currentTimeMillis() - start;
@@ -336,13 +419,14 @@ public class DecayingBloomFilter {
                            + falsePositives + " false positives");
 
     }
-    private static void testByBytes(int kbps, int numRuns) {
+
+    private static void testByBytes(int kbps, int m, int numRuns) {
         byte iv[][] = new byte[60*10*kbps][16];
         Random r = new Random();
         for (int i = 0; i < iv.length; i++)
             r.nextBytes(iv[i]);
 
-        DecayingBloomFilter filter = new DecayingBloomFilter(I2PAppContext.getGlobalContext(), 600*1000, 16);
+        DecayingBloomFilter filter = new DecayingBloomFilter(I2PAppContext.getGlobalContext(), 600*1000, 16, "test", m);
         int falsePositives = 0;
         long totalTime = 0;
         double fpr = 0d;
@@ -351,7 +435,7 @@ public class DecayingBloomFilter {
             for (int i = 0; i < iv.length; i++) {
                 if (filter.add(iv[i])) {
                     falsePositives++;
-                    System.out.println("False positive " + falsePositives + " (testByBytes j=" + j + " i=" + i + ")");
+                    //System.out.println("False positive " + falsePositives + " (testByBytes j=" + j + " i=" + i + ")");
                 }
             }
             totalTime += System.currentTimeMillis() - start;
diff --git a/core/java/src/net/i2p/util/DecayingHashSet.java b/core/java/src/net/i2p/util/DecayingHashSet.java
index 4a10f994e29eac1360f46790070fcd3a645aec4e..d0c338dfd02b7a24508909c52a8b3af107c83d55 100644
--- a/core/java/src/net/i2p/util/DecayingHashSet.java
+++ b/core/java/src/net/i2p/util/DecayingHashSet.java
@@ -1,8 +1,6 @@
 package net.i2p.util;
 
 import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.Random;
 
 import net.i2p.I2PAppContext;
@@ -62,8 +60,6 @@ import net.i2p.data.DataHelper;
 public class DecayingHashSet extends DecayingBloomFilter {
     private ConcurrentHashSet<ArrayWrapper> _current;
     private ConcurrentHashSet<ArrayWrapper> _previous;
-    /** synchronize against this lock when switching double buffers */
-    private final ReentrantReadWriteLock _reorganizeLock = new ReentrantReadWriteLock(true);
    
     /**
      * Create a double-buffered hash set that will decay its entries over time.  
@@ -82,35 +78,16 @@ public class DecayingHashSet extends DecayingBloomFilter {
             throw new IllegalArgumentException("Bad size");
         _current = new ConcurrentHashSet(128);
         _previous = new ConcurrentHashSet(128);
-        _decayEvent = new DecayEvent();
-        _keepDecaying = true;
-        SimpleScheduler.getInstance().addEvent(_decayEvent, _durationMs);
         if (_log.shouldLog(Log.WARN))
            _log.warn("New DHS " + name + " entryBytes = " + entryBytes +
                      " cycle (s) = " + (durationMs / 1000));
         // try to get a handle on memory usage vs. false positives
         context.statManager().createRateStat("router.decayingHashSet." + name + ".size",
-             "Size", "Router", new long[] { Math.max(60*1000, durationMs) });
+             "Size", "Router", new long[] { 10 * Math.max(60*1000, durationMs) });
         context.statManager().createRateStat("router.decayingHashSet." + name + ".dups",
-             "1000000 * Duplicates/Size", "Router", new long[] { Math.max(60*1000, durationMs) });
-        context.addShutdownTask(new Shutdown());
+             "1000000 * Duplicates/Size", "Router", new long[] { 10 * Math.max(60*1000, durationMs) });
     }
     
-    /**
-     * @since 0.8.8
-     */
-    private class Shutdown implements Runnable {
-        public void run() {
-            clear();
-        }
-    }
-
-    /** unsynchronized but only used for logging elsewhere */
-    @Override
-    public int getInsertedCount() { 
-        return _current.size() + _previous.size(); 
-    }
-
     /** pointless, only used for logging elsewhere */
     @Override
     public double getFalsePositiveRate() { 
@@ -166,19 +143,19 @@ public class DecayingHashSet extends DecayingBloomFilter {
     }
     
     /**
-     *  @param addIfNew if true, add the element to current if it is not already there;
+     *  @param addIfNew if true, add the element to current if it is not already there or in previous;
      *                  if false, only check
      *  @return if the element is in either the current or previous set
      */
     private boolean locked_add(ArrayWrapper w, boolean addIfNew) {
-        boolean seen;
-        // only access _current once. This adds to _current even if seen in _previous.
-        if (addIfNew)
-            seen = !_current.add(w);
-        else
-            seen = _current.contains(w);
-        if (!seen)
-            seen = _previous.contains(w);
+        boolean seen = _previous.contains(w);
+        // only access _current once.
+        if (!seen) {
+            if (addIfNew)
+                seen = !_current.add(w);
+            else
+                seen = _current.contains(w);
+        }
         if (seen) {
             // why increment if addIfNew == false? Only used for stats...
             _currentDuplicates++;
@@ -200,7 +177,8 @@ public class DecayingHashSet extends DecayingBloomFilter {
         clear();
     }
     
-    private void decay() {
+    @Override
+    protected void decay() {
         int currentCount = 0;
         long dups = 0;
         if (!getWriteLock())
@@ -219,45 +197,12 @@ public class DecayingHashSet extends DecayingBloomFilter {
             _log.debug("Decaying the filter " + _name + " after inserting " + currentCount 
                        + " elements and " + dups + " false positives");
         _context.statManager().addRateData("router.decayingHashSet." + _name + ".size",
-                                           currentCount, 0);
+                                           currentCount);
         if (currentCount > 0)
             _context.statManager().addRateData("router.decayingHashSet." + _name + ".dups",
-                                               1000l*1000*dups/currentCount, 0);
+                                               1000l*1000*dups/currentCount);
     }
     
-    /** if decay() ever blows up, we won't reschedule, and will grow unbounded, but it seems unlikely */
-    private class DecayEvent implements SimpleTimer.TimedEvent {
-        public void timeReached() {
-            if (_keepDecaying) {
-                decay();
-                SimpleScheduler.getInstance().addEvent(DecayEvent.this, _durationMs);
-            }
-        }
-    }
-
-    private void getReadLock() {
-        _reorganizeLock.readLock().lock();
-    }
-
-    private void releaseReadLock() {
-        _reorganizeLock.readLock().unlock();
-    }
-
-    /** @return true if the lock was acquired */
-    private boolean getWriteLock() {
-        try {
-            boolean rv = _reorganizeLock.writeLock().tryLock(5000, TimeUnit.MILLISECONDS);
-            if (!rv)
-                _log.error("no lock, size is: " + _reorganizeLock.getQueueLength(), new Exception("rats"));
-            return rv;
-        } catch (InterruptedException ie) {}
-        return false;
-    }
-
-    private void releaseWriteLock() {
-        _reorganizeLock.writeLock().unlock();
-    }
-
     /**
      *  This saves the data as-is if the length is <= 8 bytes,
      *  otherwise it stores an 8-byte hash.
diff --git a/core/java/src/org/xlattice/crypto/filters/BloomSHA1.java b/core/java/src/org/xlattice/crypto/filters/BloomSHA1.java
index 1e42f991f06bef60da2d6156fb4d253c8ffad19b..268ef6d9b4b97194879cd5bb5bcceb379bfb7ce1 100644
--- a/core/java/src/org/xlattice/crypto/filters/BloomSHA1.java
+++ b/core/java/src/org/xlattice/crypto/filters/BloomSHA1.java
@@ -1,6 +1,9 @@
-/* BloomSHA1.java */
 package org.xlattice.crypto.filters;
 
+import java.util.Arrays;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
 /**
  * A Bloom filter for sets of SHA1 digests.  A Bloom filter uses a set
  * of k hash functions to determine set membership.  Each hash function
@@ -31,6 +34,13 @@ package org.xlattice.crypto.filters;
  * 
  * minor tweaks by jrandom, exposing unsynchronized access and 
  * allowing larger M and K.  changes released into the public domain.
+ * 
+ * Note that this is used only by DecayingBloomFilter, which uses only
+ * the unsynchronized locked_foo() methods.
+ * 
+ * As of 0.8.11, the locked_foo() methods are thread-safe, in that they work,
+ * but there is a minor risk of false-negatives if two threads are
+ * accessing the same bloom filter integer.
  */
 
 public class BloomSHA1 {
@@ -39,14 +49,14 @@ public class BloomSHA1 {
     protected int count;
    
     protected final int[] filter;
-    protected KeySelector ks;
-    protected final int[] wordOffset;
-    protected final int[] bitOffset;
+    protected final KeySelector ks;
     
     // convenience variables
     protected final int filterBits;
     protected final int filterWords;
     
+    private final BlockingQueue<int[]> buf;
+
 /* (24,11) too big - see KeySelector
 
     public static void main(String args[]) {
@@ -80,15 +90,11 @@ public class BloomSHA1 {
         //}
         this.m = m;
         this.k = k;
-        count = 0;
         filterBits = 1 << m;
         filterWords = (filterBits + 31)/32;     // round up 
         filter = new int[filterWords];
-        doClear();
-        // offsets into the filter
-        wordOffset = new int[k];
-        bitOffset  = new int[k];
-        ks = new KeySelector(m, k, bitOffset, wordOffset);
+        ks = new KeySelector(m, k);
+        buf = new LinkedBlockingQueue(16);
 
         // DEBUG
         //System.out.println("Bloom constructor: m = " + m + ", k = " + k
@@ -114,9 +120,7 @@ public class BloomSHA1 {
     }
     /** Clear the filter, unsynchronized */
     protected void doClear() {
-        for (int i = 0; i < filterWords; i++) {
-            filter[i] = 0;
-        }
+        Arrays.fill(filter, 0);
         count = 0;
     }
     /** Synchronized version */
@@ -154,19 +158,25 @@ public class BloomSHA1 {
      * @param b byte array representing a key (SHA1 digest)
      */
     public void insert (byte[]b) { insert(b, 0, b.length); }
+
     public void insert (byte[]b, int offset, int len) {
         synchronized(this) {
-            locked_insert(b);
+            locked_insert(b, offset, len);
         }
     }
 
     public final void locked_insert(byte[]b) { locked_insert(b, 0, b.length); }
+
     public final void locked_insert(byte[]b, int offset, int len) { 
-        ks.getOffsets(b, offset, len);
+        int[] bitOffset = acquire();
+        int[] wordOffset = acquire();
+        ks.getOffsets(b, offset, len, bitOffset, wordOffset);
         for (int i = 0; i < k; i++) {
             filter[wordOffset[i]] |=  1 << bitOffset[i];
         }
         count++;
+        buf.offer(bitOffset);
+        buf.offer(wordOffset);
     }
     
     /**
@@ -176,13 +186,20 @@ public class BloomSHA1 {
      * @return true if b is in the filter 
      */
     protected final boolean isMember(byte[] b) { return isMember(b, 0, b.length); }
+
     protected final boolean isMember(byte[] b, int offset, int len) {
-        ks.getOffsets(b, offset, len);
+        int[] bitOffset = acquire();
+        int[] wordOffset = acquire();
+        ks.getOffsets(b, offset, len, bitOffset, wordOffset);
         for (int i = 0; i < k; i++) {
             if (! ((filter[wordOffset[i]] & (1 << bitOffset[i])) != 0) ) {
+                buf.offer(bitOffset);
+                buf.offer(wordOffset);
                 return false;
             }
         }
+        buf.offer(bitOffset);
+        buf.offer(wordOffset);
         return true;
     }
     
@@ -202,6 +219,75 @@ public class BloomSHA1 {
         }
     }
 
+    /**
+     * Get the bloom filter offsets for reuse.
+     * Caller should call rv.release() when done.
+     * @since 0.8.11
+     */
+    public FilterKey getFilterKey(byte[] b, int offset, int len) {
+        int[] bitOffset = acquire();
+        int[] wordOffset = acquire();
+        ks.getOffsets(b, offset, len, bitOffset, wordOffset);
+        return new FilterKey(bitOffset, wordOffset);
+    }
+
+    /**
+     * Add the key to the filter.
+     * @since 0.8.11
+     */
+    public void locked_insert(FilterKey fk) {
+        for (int i = 0; i < k; i++) {
+            filter[fk.wordOffset[i]] |=  1 << fk.bitOffset[i];
+        }
+        count++;
+    }
+
+
+    /**
+     * Is the key in the filter.
+     * @since 0.8.11
+     */
+    public boolean locked_member(FilterKey fk) {
+        for (int i = 0; i < k; i++) {
+            if (! ((filter[fk.wordOffset[i]] & (1 << fk.bitOffset[i])) != 0) )
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * @since 0.8.11
+     */
+    private int[] acquire() {
+        int[] rv = buf.poll();
+        if (rv != null)
+            return rv;
+        return new int[k];
+    }
+
+    /**
+     * @since 0.8.11
+     */
+    public void release(FilterKey fk) {
+        buf.offer(fk.bitOffset);
+        buf.offer(fk.wordOffset);
+    }
+
+    /**
+     * Store the (opaque) bloom filter offsets for reuse.
+     * @since 0.8.11
+     */
+    public static class FilterKey {
+
+        private final int[] bitOffset;
+        private final int[] wordOffset;
+
+        private FilterKey(int[] bitOffset, int[] wordOffset) {
+            this.bitOffset = bitOffset;
+            this.wordOffset = wordOffset;
+        }
+    }
+
     /** 
      * @param n number of set members
      * @return approximate false positive rate
@@ -215,6 +301,8 @@ public class BloomSHA1 {
     public final double falsePositives() {
         return falsePositives(count);
     }
+
+/*****
     // DEBUG METHODS
     public static String keyToString(byte[] key) {
         StringBuilder sb = new StringBuilder().append(key[0]);
@@ -223,23 +311,32 @@ public class BloomSHA1 {
         }
         return sb.toString();
     }
+*****/
+
     /** convert 64-bit integer to hex String */
+/*****
     public static String ltoh (long i) {
         StringBuilder sb = new StringBuilder().append("#")
                                 .append(Long.toString(i, 16));
         return sb.toString();
     }
+*****/
 
     /** convert 32-bit integer to String */
+/*****
     public static String itoh (int i) {
         StringBuilder sb = new StringBuilder().append("#")
                                 .append(Integer.toString(i, 16));
         return sb.toString();
     }
+*****/
+
     /** convert single byte to String */
+/*****
     public static String btoh (byte b) {
         int i = 0xff & b;
         return itoh(i);
     }
+*****/
 }
 
diff --git a/core/java/src/org/xlattice/crypto/filters/KeySelector.java b/core/java/src/org/xlattice/crypto/filters/KeySelector.java
index 64c6a72ba946207431cfed135f80cf0cd936fe7f..dc430ea2ff406eb6b787acce1fee65ed8b889aa4 100644
--- a/core/java/src/org/xlattice/crypto/filters/KeySelector.java
+++ b/core/java/src/org/xlattice/crypto/filters/KeySelector.java
@@ -1,4 +1,3 @@
-/* KeySelector.java */
 package org.xlattice.crypto.filters;
 
 /**
@@ -12,25 +11,34 @@ package org.xlattice.crypto.filters;
  * 
  * minor tweaks by jrandom, exposing unsynchronized access and 
  * allowing larger M and K.  changes released into the public domain.
+ *
+ * As of 0.8.11, bitoffset and wordoffset out parameters moved from fields
+ * to selector arguments, to allow concurrency.
+ * ALl methods are now thread-safe.
  */
 public class KeySelector {
    
-    private int m;
-    private int k;
-    private byte[] b;
-    private int offset; // index into b to select
-    private int length; // length into b to select
-    private int[] bitOffset;
-    private int[] wordOffset;
-    private BitSelector  bitSel;
-    private WordSelector wordSel;
+    private final int m;
+    private final int k;
+    private final BitSelector  bitSel;
+    private final WordSelector wordSel;
     
     public interface BitSelector {
-        public void getBitSelectors();
+        /**
+         *  @param bitOffset Out parameter of length k
+         *  @since 0.8.11 out parameter added
+         */
+        public void getBitSelectors(byte[] b, int offset, int length, int[] bitOffset);
     }
+
     public interface WordSelector {
-        public void getWordSelectors();
+        /**
+         *  @param wordOffset Out parameter of length k
+         *  @since 0.8.11 out parameter added
+         */
+        public void getWordSelectors(byte[] b, int offset, int length, int[] wordOffset);
     }
+
     /** AND with byte to expose index-many bits */
     public final static int[] UNMASK = { 
  // 0  1  2  3   4   5   6    7    8   9     10   11     12    13     14     15
@@ -49,8 +57,6 @@ public class KeySelector {
      *
      * @param m    size of the filter as a power of 2
      * @param k    number of 'hash functions'
-     * @param bitOffset array of k bit offsets (offset of flag bit in word)
-     * @param wordOffset array of k word offsets (offset of word flag is in)
      *
      * Note that if k and m are too big, the GenericWordSelector blows up -
      * The max for 32-byte keys is m=23 and k=11.
@@ -59,15 +65,13 @@ public class KeySelector {
      *
      * It isn't clear how to fix this.
      */
-    public KeySelector (int m, int k, int[] bitOffset, int [] wordOffset) {
+    public KeySelector (int m, int k) {
         //if ( (m < 2) || (m > 20)|| (k < 1) 
         //             || (bitOffset == null) || (wordOffset == null)) {
         //    throw new IllegalArgumentException();
         //}
         this.m = m;
         this.k = k;
-        this.bitOffset = bitOffset;
-        this.wordOffset = wordOffset;
         bitSel  = new GenericBitSelector();
         wordSel = new GenericWordSelector();
     }
@@ -78,7 +82,7 @@ public class KeySelector {
      */
     public class GenericBitSelector implements BitSelector {
         /** Do the extraction */
-        public void getBitSelectors() {
+        public void getBitSelectors(byte[] b, int offset, int length, int[] bitOffset) {
             int curBit = 8 * offset; 
             int curByte;
             for (int j = 0; j < k; j++) {
@@ -132,7 +136,7 @@ public class KeySelector {
      */
     public class GenericWordSelector implements WordSelector {
         /** Extract the k offsets into the word offset array */
-        public void getWordSelectors() {
+        public void getWordSelectors(byte[] b, int offset, int length, int[] wordOffset) {
             int stride = m - 5;
             //assert true: stride<16;
             int curBit = (k * 5) + (offset * 8); 
@@ -221,32 +225,47 @@ public class KeySelector {
             }
         } 
     }
+
+    /**
+     * Given a key, populate the word and bit offset arrays, each
+     * of which has k elements.
+     * 
+     * @param key cryptographic key used in populating the arrays
+     * @param bitOffset Out parameter of length k
+     * @param wordOffset Out parameter of length k
+     * @since 0.8.11 out parameters added
+     */
+    public void getOffsets (byte[] key, int[] bitOffset, int[] wordOffset) {
+        getOffsets(key, 0, key.length, bitOffset, wordOffset);
+    }
+
     /**
      * Given a key, populate the word and bit offset arrays, each
      * of which has k elements.
      * 
      * @param key cryptographic key used in populating the arrays
+     * @param bitOffset Out parameter of length k
+     * @param wordOffset Out parameter of length k
+     * @since 0.8.11 out parameters added
      */
-    public void getOffsets (byte[] key) { getOffsets(key, 0, key.length); }
-    public void getOffsets (byte[] key, int off, int len) {
-        if (key == null) {
-            throw new IllegalArgumentException("null key");
-        }
-        if (len < 20) {
-            throw new IllegalArgumentException(
-                "key must be at least 20 bytes long");
-        }
-        b = key;
-        offset = off;
-        length = len;
+    public void getOffsets (byte[] key, int off, int len, int[] bitOffset, int[] wordOffset) {
+        // skip these checks for speed
+        //if (key == null) {
+        //    throw new IllegalArgumentException("null key");
+        //}
+        //if (len < 20) {
+        //    throw new IllegalArgumentException(
+        //        "key must be at least 20 bytes long");
+        //}
 //      // DEBUG
 //      System.out.println("KeySelector.getOffsets for " 
 //                                          + BloomSHA1.keyToString(b));
 //      // END
-        bitSel.getBitSelectors();
-        wordSel.getWordSelectors();
+        bitSel.getBitSelectors(key, off, len, bitOffset);
+        wordSel.getWordSelectors(key, off, len, wordOffset);
     }
 
+/*****
     // DEBUG METHODS ////////////////////////////////////////////////
     String itoh(int i) {
         return BloomSHA1.itoh(i);
@@ -254,6 +273,7 @@ public class KeySelector {
     String btoh(byte b) {
         return BloomSHA1.btoh(b);
     }
+*****/
 }