--- old/src/java.base/share/classes/sun/security/ssl/SessionId.java 2017-11-22 10:20:33.180131472 +0100 +++ new/src/java.base/share/classes/sun/security/ssl/SessionId.java 2017-11-22 10:20:32.954135385 +0100 @@ -27,6 +27,7 @@ package sun.security.ssl; import java.security.SecureRandom; +import java.util.Arrays; import javax.net.ssl.SSLProtocolException; /** @@ -91,30 +92,15 @@ @Override public int hashCode () { - int retval = 0; - - for (int i = 0; i < sessionId.length; i++) - retval += sessionId [i]; - return retval; + return Arrays.hashCode(sessionId); } /** Returns true if the parameter is the same session ID */ @Override public boolean equals (Object obj) { - if (!(obj instanceof SessionId)) - return false; - - SessionId s = (SessionId) obj; - byte[] b = s.getId (); - - if (b.length != sessionId.length) - return false; - for (int i = 0; i < sessionId.length; i++) { - if (b [i] != sessionId [i]) - return false; - } - return true; + return obj instanceof SessionId && + Arrays.equals(sessionId, ((SessionId)obj).sessionId); } /** --- old/src/java.base/share/classes/sun/security/util/Cache.java 2017-11-22 10:20:33.868119558 +0100 +++ new/src/java.base/share/classes/sun/security/util/Cache.java 2017-11-22 10:20:33.636123575 +0100 @@ -27,6 +27,10 @@ import java.util.*; import java.lang.ref.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; /** * Abstract base class and factory for caches. A cache is a key-value mapping. @@ -240,91 +244,91 @@ class MemoryCache extends Cache { - private static final float LOAD_FACTOR = 0.75f; - // XXXX private static final boolean DEBUG = false; - private final Map> cacheMap; - private int maxSize; - private long lifetime; + private final ConcurrentMap> cacheMap; + private CacheEntry lruEntry, mruEntry; + private volatile int maxSize; + private volatile long lifetime; // ReferenceQueue is of type V instead of Cache // to allow SoftCacheEntry to extend SoftReference private final ReferenceQueue queue; public MemoryCache(boolean soft, int maxSize) { - this(soft, maxSize, 0); + this(soft, maxSize, 0 /* no time out */); } - public MemoryCache(boolean soft, int maxSize, int lifetime) { + public MemoryCache(boolean soft, int maxSize, int timeout) { this.maxSize = maxSize; - this.lifetime = lifetime * 1000; + this.lifetime = timeout < 0 ? 0 : TimeUnit.SECONDS.toNanos(timeout); if (soft) this.queue = new ReferenceQueue<>(); else this.queue = null; - int buckets = (int)(maxSize / LOAD_FACTOR) + 1; - cacheMap = new LinkedHashMap<>(buckets, LOAD_FACTOR, true); + cacheMap = new ConcurrentHashMap<>(maxSize); } /** - * Empty the reference queue and remove all corresponding entries + * Drain the reference queue and remove all corresponding entries * from the cache. * * This method should be called at the beginning of each public * method. */ - private void emptyQueue() { + private void drainQueue() { if (queue == null) { return; } - int startSize = cacheMap.size(); + int removedCount = 0; while (true) { @SuppressWarnings("unchecked") - CacheEntry entry = (CacheEntry)queue.poll(); - if (entry == null) { + CacheEntry ce = (CacheEntry)queue.poll(); + if (ce == null) { break; } - K key = entry.getKey(); - if (key == null) { - // key is null, entry has already been removed - continue; - } - CacheEntry currentEntry = cacheMap.remove(key); - // check if the entry in the map corresponds to the expired - // entry. If not, readd the entry - if ((currentEntry != null) && (entry != currentEntry)) { - cacheMap.put(key, currentEntry); + if (ce.unlink(this)) { + if (cacheMap.remove(ce.getKey(), ce)) { + removedCount++; + } + // the one who unlinks is responsible for invalidating + ce.invalidate(); } } if (DEBUG) { - int endSize = cacheMap.size(); - if (startSize != endSize) { - System.out.println("*** Expunged " + (startSize - endSize) - + " entries, " + endSize + " entries left"); + if (removedCount > 0) { + System.out.println("*** Expunged " + removedCount + + " entries, " + cacheMap.size() + " entries left"); } } } /** - * Scan all entries and remove all expired ones. + * Drain the reference queue and remove all corresponding entries + * from the cache. Then expunge all expired entries. + * + * This method should be called just before doing any decisions based + * on the number of entries remaining in cache (i.e. cacheMap.size()). */ - private void expungeExpiredEntries() { - emptyQueue(); + private void drainQueueExpungeExpired() { + drainQueue(); + int cnt = 0; + long currentTime = System.nanoTime(); + long lifetime = this.lifetime; if (lifetime == 0) { return; } - int cnt = 0; - long time = System.currentTimeMillis(); - for (Iterator> t = cacheMap.values().iterator(); - t.hasNext(); ) { - CacheEntry entry = t.next(); - if (entry.isValid(time) == false) { - t.remove(); + Predicate> entryIsInvalid = + ce -> !ce.isValid(currentTime, lifetime); + CacheEntry lru; + while ((lru = CacheEntry.unlinkLruIf(this, entryIsInvalid)) != null) { + if (cacheMap.remove(lru.getKey(), lru)) { cnt++; } + // the one who unlinks is responsible for invalidating + lru.invalidate(); } if (DEBUG) { if (cnt != 0) { @@ -334,136 +338,143 @@ } } - public synchronized int size() { - expungeExpiredEntries(); + /** + * Remove LRU entries while cache size is greater than given {@code maxSize}. + * + * @param maxSize pack cache to size no more than + */ + private void reduceToMaxSize(int maxSize) { + if (maxSize > 0 && cacheMap.size() > maxSize) { + // 1st get rid of all cleared and expired entries + drainQueueExpungeExpired(); + while (cacheMap.size() > maxSize) { // while still too large + CacheEntry lru = CacheEntry.unlinkLruIf(this, ce -> true); + if (lru != null) { + if (DEBUG) { + System.out.println("** Overflow removal " + + lru.getKey() + " | " + lru.getValue()); + } + cacheMap.remove(lru.getKey(), lru); + // the one who unlinks is responsible for invalidating + lru.invalidate(); + } + } + } + } + + public int size() { + drainQueueExpungeExpired(); return cacheMap.size(); } - public synchronized void clear() { - if (queue != null) { - // if this is a SoftReference cache, first invalidate() all - // entries so that GC does not have to enqueue them - for (CacheEntry entry : cacheMap.values()) { - entry.invalidate(); - } - while (queue.poll() != null) { - // empty - } + public void clear() { + CacheEntry lru; + while ((lru = CacheEntry.unlinkLruIf(this, ce -> true)) != null) { + cacheMap.remove(lru.getKey(), lru); + // the one who unlinks is responsible for invalidating + lru.invalidate(); } - cacheMap.clear(); } - public synchronized void put(K key, V value) { - emptyQueue(); - long expirationTime = (lifetime == 0) ? 0 : - System.currentTimeMillis() + lifetime; - CacheEntry newEntry = newEntry(key, value, expirationTime, queue); + public void put(K key, V value) { + drainQueue(); + CacheEntry newEntry = newEntry(key, value, queue); + newEntry.link(this); CacheEntry oldEntry = cacheMap.put(key, newEntry); - if (oldEntry != null) { + if (oldEntry != null && oldEntry.unlink(this)) { + // the one who unlinks is responsible for invalidating oldEntry.invalidate(); return; } - if (maxSize > 0 && cacheMap.size() > maxSize) { - expungeExpiredEntries(); - if (cacheMap.size() > maxSize) { // still too large? - Iterator> t = cacheMap.values().iterator(); - CacheEntry lruEntry = t.next(); - if (DEBUG) { - System.out.println("** Overflow removal " - + lruEntry.getKey() + " | " + lruEntry.getValue()); - } - t.remove(); - lruEntry.invalidate(); - } - } + reduceToMaxSize(maxSize); } - public synchronized V get(Object key) { - emptyQueue(); + public V get(Object key) { + drainQueue(); CacheEntry entry = cacheMap.get(key); if (entry == null) { return null; } - long time = (lifetime == 0) ? 0 : System.currentTimeMillis(); - if (entry.isValid(time) == false) { + // harmless data race: entry.isValid() vs. entry.invalidate() + if (!entry.isValid(System.nanoTime(), lifetime)) { if (DEBUG) { System.out.println("Ignoring expired entry"); } - cacheMap.remove(key); + if (entry.unlink(this)) { + cacheMap.remove(entry.getKey(), entry); + // the one who unlinks is responsible for invalidating + entry.invalidate(); + } return null; } - return entry.getValue(); + return entry.getValue(); // harmless data race with entry.invalidate() } - public synchronized void remove(Object key) { - emptyQueue(); - CacheEntry entry = cacheMap.remove(key); + public void remove(Object key) { + drainQueue(); + CacheEntry entry = cacheMap.get(key); if (entry != null) { - entry.invalidate(); - } - } - - public synchronized void setCapacity(int size) { - expungeExpiredEntries(); - if (size > 0 && cacheMap.size() > size) { - Iterator> t = cacheMap.values().iterator(); - for (int i = cacheMap.size() - size; i > 0; i--) { - CacheEntry lruEntry = t.next(); - if (DEBUG) { - System.out.println("** capacity reset removal " - + lruEntry.getKey() + " | " + lruEntry.getValue()); - } - t.remove(); - lruEntry.invalidate(); + if (entry.unlink(this)) { + cacheMap.remove(entry.getKey(), entry); + // the one who unlinks is responsible for invalidating + entry.invalidate(); } } + } - maxSize = size > 0 ? size : 0; + public void setCapacity(int size) { + if (size < 0) size = 0; + maxSize = size; + // in case maxSize was reduces, immediately reduce the cache too + reduceToMaxSize(size); if (DEBUG) { System.out.println("** capacity reset to " + size); } } - public synchronized void setTimeout(int timeout) { - emptyQueue(); - lifetime = timeout > 0 ? timeout * 1000L : 0L; - + public void setTimeout(int timeout) { + this.lifetime = timeout < 0 ? 0 : TimeUnit.SECONDS.toNanos(timeout); + // in case timeout was shortened, immediately expunge newly expired entries + drainQueueExpungeExpired(); if (DEBUG) { - System.out.println("** lifetime reset to " + timeout); + System.out.println("** lifetime reset to " + lifetime + " nanos"); } } // it is a heavyweight method. - public synchronized void accept(CacheVisitor visitor) { - expungeExpiredEntries(); + public void accept(CacheVisitor visitor) { Map cached = getCachedEntries(); - visitor.visit(cached); } + // return a snapshot of the valid/non-expired part of the cache private Map getCachedEntries() { + drainQueueExpungeExpired(); Map kvmap = new HashMap<>(cacheMap.size()); for (CacheEntry entry : cacheMap.values()) { - kvmap.put(entry.getKey(), entry.getValue()); + K key = entry.getKey(); // harmless data race with entry.invalidate() + V val = entry.getValue(); // harmless data race with entry.invalidate() + if (key != null && val != null) { + kvmap.put(key, val); + } } return kvmap; } - protected CacheEntry newEntry(K key, V value, - long expirationTime, ReferenceQueue queue) { + protected CacheEntry newEntry(K key, V value, ReferenceQueue queue) { if (queue != null) { - return new SoftCacheEntry<>(key, value, expirationTime, queue); + return new SoftCacheEntry<>(key, value, queue); } else { - return new HardCacheEntry<>(key, value, expirationTime); + return new HardCacheEntry<>(key, value); } } - private static interface CacheEntry { + private interface CacheEntry { - boolean isValid(long currentTime); + boolean isValid(long currentTime, long lifetime); void invalidate(); @@ -471,18 +482,81 @@ V getValue(); + // double-linked-list management + + CacheEntry prev(); + + CacheEntry next(); + + void setPrev(CacheEntry newPrev); + + void setNext(CacheEntry newNext); + + void entryLinked(); + + default void link(MemoryCache memoryCache) { + synchronized (memoryCache) { + assert prev() == this && next() == this : "Entry already linked"; + if (memoryCache.lruEntry == null) { + memoryCache.lruEntry = memoryCache.mruEntry = this; + setPrev(null); setNext(null); + } else { + setPrev(memoryCache.mruEntry); + memoryCache.mruEntry.setNext(this); + memoryCache.mruEntry = this; + setNext(null); + } + entryLinked(); + } + } + + default boolean unlink(MemoryCache memoryCache) { + synchronized (memoryCache) { + CacheEntry next = next(); + CacheEntry prev = prev(); + if (next == this && prev == this) { + // not linked + return false; + } + if (memoryCache.lruEntry == this) { + memoryCache.lruEntry = next; + } + if (memoryCache.mruEntry == this) { + memoryCache.mruEntry = prev; + } + if (prev != null) { + prev.setNext(next); + } + if (next != null) { + next.setPrev(prev); + } + setPrev(this); + setNext(this); + return true; + } + } + + static CacheEntry unlinkLruIf(MemoryCache memoryCache, Predicate> predicate) { + synchronized (memoryCache) { + CacheEntry lru = memoryCache.lruEntry; + if (lru == null || !predicate.test(lru)) { + return null; + } + return lru.unlink(memoryCache) ? lru : null; + } + } } private static class HardCacheEntry implements CacheEntry { private K key; private V value; - private long expirationTime; + private long linkTime; + private CacheEntry prev = this, next = this; - HardCacheEntry(K key, V value, long expirationTime) { + HardCacheEntry(K key, V value) { this.key = key; this.value = value; - this.expirationTime = expirationTime; } public K getKey() { @@ -493,18 +567,42 @@ return value; } - public boolean isValid(long currentTime) { - boolean valid = (currentTime <= expirationTime); - if (valid == false) { - invalidate(); - } - return valid; + public boolean isValid(long currentTime, long lifetime) { + return value != null && + (lifetime == 0 || (currentTime - linkTime) <= lifetime); } public void invalidate() { key = null; value = null; - expirationTime = -1; + } + + @Override + public CacheEntry prev() { + return prev; + } + + @Override + public CacheEntry next() { + return next; + } + + @Override + public void setPrev(CacheEntry newPrev) { + prev = newPrev; + } + + @Override + public void setNext(CacheEntry newNext) { + next = newNext; + } + + @Override + public void entryLinked() { + // sample link time while synchronized which guarantees + // monotonic time increment (no decrement), so dl-list + // is kept sorted by linkTime + linkTime = System.nanoTime(); } } @@ -513,13 +611,12 @@ implements CacheEntry { private K key; - private long expirationTime; + private long linkTime; + private CacheEntry prev = this, next = this; - SoftCacheEntry(K key, V value, long expirationTime, - ReferenceQueue queue) { + SoftCacheEntry(K key, V value, ReferenceQueue queue) { super(value, queue); this.key = key; - this.expirationTime = expirationTime; } public K getKey() { @@ -530,18 +627,42 @@ return get(); } - public boolean isValid(long currentTime) { - boolean valid = (currentTime <= expirationTime) && (get() != null); - if (valid == false) { - invalidate(); - } - return valid; + public boolean isValid(long currentTime, long lifetime) { + return get() != null && + (lifetime == 0 || (currentTime - linkTime) <= lifetime); } public void invalidate() { - clear(); key = null; - expirationTime = -1; + clear(); + } + + @Override + public CacheEntry prev() { + return prev; + } + + @Override + public CacheEntry next() { + return next; + } + + @Override + public void setPrev(CacheEntry newPrev) { + prev = newPrev; + } + + @Override + public void setNext(CacheEntry newNext) { + next = newNext; + } + + @Override + public void entryLinked() { + // sample link time while synchronized which guarantees + // monotonic time increment (no decrement), so dl-list + // is kept sorted by linkTime + linkTime = System.nanoTime(); } }