From f175031952824b7a5bbf125382cf472f4627c0fb Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Mon, 29 Apr 2024 01:41:13 -0400 Subject: [PATCH] - Added the SealableXXX collection classes. - Strengthened the implementation of ConcurrentList and ConcurrentHashSet. --- changelog.md | 5 +- .../cedarsoftware/util/ConcurrentList.java | 259 ++++-------------- .../com/cedarsoftware/util/ConcurrentSet.java | 11 +- .../java/com/cedarsoftware/util/LRUCache.java | 234 +++++++++------- .../com/cedarsoftware/util/SealableList.java | 2 +- .../com/cedarsoftware/util/SealableMap.java | 2 +- .../util/SealableNavigableMap.java | 2 +- .../util/SealableNavigableSet.java | 2 +- .../com/cedarsoftware/util/SealableSet.java | 2 +- .../com/cedarsoftware/util/LRUCacheTest.java | 49 +++- 10 files changed, 232 insertions(+), 336 deletions(-) diff --git a/changelog.md b/changelog.md index 9ef86dab..69de2261 100644 --- a/changelog.md +++ b/changelog.md @@ -6,12 +6,13 @@ * Added `SealableNavigableSet` similar to SealableList but with `NavigableSet` nature. * Added `SealableNavigableMap` similar to SealableList but with `NavigableMap` nature. * Updated `ConcurrentList` to support wrapping any `List` and making it thread-safe, including all view APIs: `iterator(),` `listIterator(),` `listIterator(index).` The no-arg constructor creates a `ConcurrentList` ready-to-go. The constructor that takes a `List` parameter constructor wraps the passed in list and makes it thread-safe. + * Renamed `ConcurrentHashSet` to `ConcurrentSet.` * 2.8.0 * Added `ClassUtilities.doesOneWrapTheOther()` API so that it is easy to test if one class is wrapping the other. * Added `StringBuilder` and `StringBuffer` to `Strings` to the `Converter.` Eliminates special cases for `.toString()` calls where generalized `convert(src, type)` is being used. * 2.7.0 - * Added `ConcurrentHashList,` which implements a thread-safe `List.` Provides all API support except for `listIterator(),` however, it implements `iterator()` which returns an iterator to a snapshot copy of the `List.` - * Added `ConcurrentHashSet,` a true `Set` which is a bit easier to use than `ConcurrentSkipListSet,` which as a `NaviableSet` and `SortedSet,` requires each element to be `Comparable.` + * Added `ConcurrentList,` which implements a thread-safe `List.` Provides all API support except for `listIterator(),` however, it implements `iterator()` which returns an iterator to a snapshot copy of the `List.` + * Added `ConcurrentHashSet,` a true `Set` which is a bit easier to use than `ConcurrentSkipListSet,` which as a `NavigableSet` and `SortedSet,` requires each element to be `Comparable.` * Performance improvement: On `LRUCache,` removed unnecessary `Collections.SynchronizedMap` surrounding the internal `LinkedHashMap` as the concurrent protection offered by `ReentrantReadWriteLock` is all that is needed. * 2.6.0 * Performance improvement: `Converter` instance creation is faster due to the code no longer copying the static default table. Overrides are kept in separate variable. diff --git a/src/main/java/com/cedarsoftware/util/ConcurrentList.java b/src/main/java/com/cedarsoftware/util/ConcurrentList.java index fd4eb426..e8212be2 100644 --- a/src/main/java/com/cedarsoftware/util/ConcurrentList.java +++ b/src/main/java/com/cedarsoftware/util/ConcurrentList.java @@ -8,6 +8,7 @@ import java.util.ListIterator; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; /** * ConcurrentList provides a List and List wrapper that is thread-safe, usable in highly concurrent @@ -38,16 +39,15 @@ */ public class ConcurrentList implements List { private final List list; - private final ReadWriteLock lock; + private final transient ReadWriteLock lock = new ReentrantReadWriteLock(); /** - * Use this no-arg constructor to create a ConcurrentList. + * No-arg constructor to create an empty ConcurrentList, wrapping an ArrayList. */ public ConcurrentList() { - lock = new ReentrantReadWriteLock(); this.list = new ArrayList<>(); } - + /** * Use this constructor to wrap a List (any kind of List) and make it a ConcurrentList. * No duplicate of the List is created and the original list is operated on directly. @@ -55,233 +55,68 @@ public ConcurrentList() { */ public ConcurrentList(List list) { if (list == null) { - throw new IllegalArgumentException("list cannot be null"); + throw new IllegalArgumentException("List cannot be null"); } - lock = new ReentrantReadWriteLock(); this.list = list; } - public int size() { - lock.readLock().lock(); - try { - return list.size(); - } finally { - lock.readLock().unlock(); - } - } - - public boolean isEmpty() { - lock.readLock().lock(); - try { - return list.isEmpty(); - } finally { - lock.readLock().unlock(); - } - } - - public boolean equals(Object obj) { - lock.readLock().lock(); - try { - return list.equals(obj); - } finally { - lock.readLock().unlock(); - } - } - - public int hashCode() { - lock.readLock().lock(); - try { - return list.hashCode(); - } finally { - lock.readLock().unlock(); - } - } - - public String toString() { - lock.readLock().lock(); - try { - return list.toString(); - } finally { - lock.readLock().unlock(); - } - } - - public boolean contains(Object o) { - lock.readLock().lock(); - try { - return list.contains(o); - } finally { - lock.readLock().unlock(); - } - } - - public Iterator iterator() { - lock.readLock().lock(); - try { - return new ArrayList<>(list).iterator(); - } finally { - lock.readLock().unlock(); - } - } - - public Object[] toArray() { - lock.readLock().lock(); - try { - return list.toArray(); - } finally { - lock.readLock().unlock(); - } - } - - public T[] toArray(T[] a) { - lock.readLock().lock(); - try { - return list.toArray(a); - } finally { - lock.readLock().unlock(); - } - } - - public boolean add(E e) { - lock.writeLock().lock(); - try { - return list.add(e); - } finally { - lock.writeLock().unlock(); - } - } - - public boolean remove(Object o) { - lock.writeLock().lock(); - try { - return list.remove(o); - } finally { - lock.writeLock().unlock(); - } - } - - public boolean containsAll(Collection c) { - lock.readLock().lock(); - try { - return new HashSet<>(list).containsAll(c); - } finally { - lock.readLock().unlock(); - } - } - - public boolean addAll(Collection c) { - lock.writeLock().lock(); - try { - return list.addAll(c); - } finally { - lock.writeLock().unlock(); - } - } - - public boolean addAll(int index, Collection c) { - lock.writeLock().lock(); - try { - return list.addAll(index, c); - } finally { - lock.writeLock().unlock(); - } - } - - public boolean removeAll(Collection c) { - lock.writeLock().lock(); - try { - return list.removeAll(c); - } finally { - lock.writeLock().unlock(); - } - } - - public boolean retainAll(Collection c) { - lock.writeLock().lock(); - try { - return list.retainAll(c); - } finally { - lock.writeLock().unlock(); - } - } - + // Immutable APIs + public boolean equals(Object other) { return readOperation(() -> list.equals(other)); } + public int hashCode() { return readOperation(list::hashCode); } + public String toString() { return readOperation(list::toString); } + public int size() { return readOperation(list::size); } + public boolean isEmpty() { return readOperation(list::isEmpty); } + public boolean contains(Object o) { return readOperation(() -> list.contains(o)); } + public boolean containsAll(Collection c) { return readOperation(() -> new HashSet<>(list).containsAll(c)); } + public E get(int index) { return readOperation(() -> list.get(index)); } + public int indexOf(Object o) { return readOperation(() -> list.indexOf(o)); } + public int lastIndexOf(Object o) { return readOperation(() -> list.lastIndexOf(o)); } + public Iterator iterator() { return readOperation(() -> new ArrayList<>(list).iterator()); } + public Object[] toArray() { return readOperation(list::toArray); } + public T[] toArray(T[] a) { return readOperation(() -> list.toArray(a)); } + + // Mutable APIs + public boolean add(E e) { return writeOperation(() -> list.add(e)); } + public boolean addAll(Collection c) { return writeOperation(() -> list.addAll(c)); } + public boolean addAll(int index, Collection c) { return writeOperation(() -> list.addAll(index, c)); } + public void add(int index, E element) { + writeOperation(() -> { + list.add(index, element); + return null; + }); + } + public E set(int index, E element) { return writeOperation(() -> list.set(index, element)); } + public E remove(int index) { return writeOperation(() -> list.remove(index)); } + public boolean remove(Object o) { return writeOperation(() -> list.remove(o)); } + public boolean removeAll(Collection c) { return writeOperation(() -> list.removeAll(c)); } + public boolean retainAll(Collection c) { return writeOperation(() -> list.retainAll(c)); } public void clear() { - lock.writeLock().lock(); - try { + writeOperation(() -> { list.clear(); - } finally { - lock.writeLock().unlock(); - } + return null; // To comply with the Supplier return type + }); } + public ListIterator listIterator() { return readOperation(() -> new ArrayList<>(list).listIterator()); } - public E get(int index) { + // Unsupported operations + public ListIterator listIterator(int index) { throw new UnsupportedOperationException("listIterator(index) not implemented for ConcurrentList"); } + public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException("subList not implemented for ConcurrentList"); } + + private T readOperation(Supplier operation) { lock.readLock().lock(); try { - return list.get(index); + return operation.get(); } finally { lock.readLock().unlock(); } } - public E set(int index, E element) { + private T writeOperation(Supplier operation) { lock.writeLock().lock(); try { - return list.set(index, element); + return operation.get(); } finally { lock.writeLock().unlock(); } } - - public void add(int index, E element) { - lock.writeLock().lock(); - try { - list.add(index, element); - } finally { - lock.writeLock().unlock(); - } - } - - public E remove(int index) { - lock.writeLock().lock(); - try { - return list.remove(index); - } finally { - lock.writeLock().unlock(); - } - } - - public int indexOf(Object o) { - lock.readLock().lock(); - try { - return list.indexOf(o); - } finally { - lock.readLock().unlock(); - } - } - - public int lastIndexOf(Object o) { - lock.readLock().lock(); - try { - return list.lastIndexOf(o); - } finally { - lock.readLock().unlock(); - } - } - - public List subList(int fromIndex, int toIndex) { - throw new UnsupportedOperationException("subList not implemented for ConcurrentList"); - } - - public ListIterator listIterator() { - lock.readLock().lock(); - try { - return new ArrayList(list).listIterator(); - } finally { - lock.readLock().unlock(); - } - } - - public ListIterator listIterator(int index) { - throw new UnsupportedOperationException("listIterator(index) not implemented for ConcurrentList"); - } } \ No newline at end of file diff --git a/src/main/java/com/cedarsoftware/util/ConcurrentSet.java b/src/main/java/com/cedarsoftware/util/ConcurrentSet.java index 1975a4eb..81e7903e 100644 --- a/src/main/java/com/cedarsoftware/util/ConcurrentSet.java +++ b/src/main/java/com/cedarsoftware/util/ConcurrentSet.java @@ -23,8 +23,17 @@ * limitations under the License. */ public class ConcurrentSet implements Set { - private final Set set = ConcurrentHashMap.newKeySet(); + private final Set set; + public ConcurrentSet() { + set = ConcurrentHashMap.newKeySet(); + } + + public ConcurrentSet(Collection col) { + set = ConcurrentHashMap.newKeySet(col.size()); + set.addAll(col); + } + // Immutable APIs public boolean equals(Object other) { return set.equals(other); } public int hashCode() { return set.hashCode(); } diff --git a/src/main/java/com/cedarsoftware/util/LRUCache.java b/src/main/java/com/cedarsoftware/util/LRUCache.java index c64cd15d..7009e502 100644 --- a/src/main/java/com/cedarsoftware/util/LRUCache.java +++ b/src/main/java/com/cedarsoftware/util/LRUCache.java @@ -1,15 +1,19 @@ package com.cedarsoftware.util; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; /** - * This class provides a Least Recently Used (LRU) cache API that will evict the least recently used items, - * once a threshold is met. It implements the Map interface for convenience. + * This class provides a thread-safe Least Recently Used (LRU) cache API that will evict the least recently used items, + * once a threshold is met. It implements the Map interface for convenience. It is thread-safe via usage of + * ReentrantReadWriteLock() around read and write APIs, including delegating to keySet(), entrySet(), and + * values() and each of their iterators. *

* @author John DeRegnaucourt (jdereg@gmail.com) *
@@ -29,7 +33,8 @@ */ public class LRUCache implements Map { private final Map cache; - private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final transient ReadWriteLock lock = new ReentrantReadWriteLock(); + private final static Object NO_ENTRY = new Object(); public LRUCache(int capacity) { cache = new LinkedHashMap(capacity, 0.75f, true) { @@ -39,124 +44,145 @@ protected boolean removeEldestEntry(Map.Entry eldest) { }; } - // Implement Map interface - public int size() { - lock.readLock().lock(); - try { - return cache.size(); - } finally { - lock.readLock().unlock(); - } - } - - public boolean isEmpty() { - lock.readLock().lock(); - try { - return cache.isEmpty(); - } finally { - lock.readLock().unlock(); - } - } - - public boolean containsKey(Object key) { - lock.readLock().lock(); - try { - return cache.containsKey(key); - } finally { - lock.readLock().unlock(); - } - } - - public boolean containsValue(Object value) { - lock.readLock().lock(); - try { - return cache.containsValue(value); - } finally { - lock.readLock().unlock(); - } - } - - public V get(Object key) { - lock.readLock().lock(); - try { - return cache.get(key); - } finally { - lock.readLock().unlock(); - } - } - - public V put(K key, V value) { - lock.writeLock().lock(); - try { - return cache.put(key, value); - } finally { - lock.writeLock().unlock(); - } - } - - public V remove(Object key) { - lock.writeLock().lock(); - try { - return cache.remove(key); - } finally { - lock.writeLock().unlock(); - } - } - - public void putAll(Map m) { - lock.writeLock().lock(); - try { - cache.putAll(m); - } finally { - lock.writeLock().unlock(); - } - } - - public void clear() { - lock.writeLock().lock(); - try { - cache.clear(); - } finally { - lock.writeLock().unlock(); - } - } + // Immutable APIs + public boolean equals(Object obj) { return readOperation(() -> cache.equals(obj)); } + public int hashCode() { return readOperation(cache::hashCode); } + public String toString() { return readOperation(cache::toString); } + public int size() { return readOperation(cache::size); } + public boolean isEmpty() { return readOperation(cache::isEmpty); } + public boolean containsKey(Object key) { return readOperation(() -> cache.containsKey(key)); } + public boolean containsValue(Object value) { return readOperation(() -> cache.containsValue(value)); } + public V get(Object key) { return readOperation(() -> cache.get(key)); } + + // Mutable APIs + public V put(K key, V value) { return writeOperation(() -> cache.put(key, value)); } + public void putAll(Map m) { writeOperation(() -> { cache.putAll(m); return null; }); } + public V putIfAbsent(K key, V value) { return writeOperation(() -> cache.putIfAbsent(key, value)); } + public V remove(Object key) { return writeOperation(() -> cache.remove(key)); } + public void clear() { writeOperation(() -> { cache.clear(); return null; }); } public Set keySet() { - lock.readLock().lock(); - try { - return cache.keySet(); - } finally { - lock.readLock().unlock(); - } + return readOperation(() -> new Set() { + public int size() { return readOperation(cache::size); } + public boolean isEmpty() { return readOperation(cache::isEmpty); } + public boolean contains(Object o) { return readOperation(() -> cache.containsKey(o)); } + public Iterator iterator() { + return new Iterator() { + private final Iterator it = cache.keySet().iterator(); + private K current = (K)NO_ENTRY; + + public boolean hasNext() { return readOperation(it::hasNext); } + public K next() { return readOperation(() -> { current = it.next(); return current; }); } + public void remove() { + writeOperation(() -> { + if (current == NO_ENTRY) { + throw new IllegalStateException("Next not called or already removed"); + } + it.remove(); // Remove from the underlying map + current = (K)NO_ENTRY; + return null; + }); + } + }; + } + public Object[] toArray() { return readOperation(() -> cache.keySet().toArray()); } + public T[] toArray(T[] a) { return readOperation(() -> cache.keySet().toArray(a)); } + public boolean add(K k) { throw new UnsupportedOperationException("add() not supported on .keySet() of a Map"); } + public boolean remove(Object o) { return writeOperation(() -> cache.remove(o) != null); } + public boolean containsAll(Collection c) { return readOperation(() -> cache.keySet().containsAll(c)); } + public boolean addAll(Collection c) { throw new UnsupportedOperationException("addAll() not supported on .keySet() of a Map"); } + public boolean retainAll(Collection c) { return writeOperation(() -> cache.keySet().retainAll(c)); } + public boolean removeAll(Collection c) { return writeOperation(() -> cache.keySet().removeAll(c)); } + public void clear() { writeOperation(() -> { cache.clear(); return null; }); } + }); } public Collection values() { - lock.readLock().lock(); - try { - return cache.values(); - } finally { - lock.readLock().unlock(); - } + return readOperation(() -> new Collection() { + public int size() { return readOperation(cache::size); } + public boolean isEmpty() { return readOperation(cache::isEmpty); } + public boolean contains(Object o) { return readOperation(() -> cache.containsValue(o)); } + public Iterator iterator() { + return new Iterator() { + private final Iterator it = cache.values().iterator(); + private V current = (V)NO_ENTRY; + + public boolean hasNext() { return readOperation(it::hasNext); } + public V next() { return readOperation(() -> { current = it.next(); return current; }); } + public void remove() { + writeOperation(() -> { + if (current == NO_ENTRY) { + throw new IllegalStateException("Next not called or already removed"); + } + it.remove(); // Remove from the underlying map + current = (V)NO_ENTRY; + return null; + }); + } + }; + } + public Object[] toArray() { return readOperation(() -> cache.values().toArray()); } + public T[] toArray(T[] a) { return readOperation(() -> cache.values().toArray(a)); } + public boolean add(V value) { throw new UnsupportedOperationException("add() not supported on values() of a Map"); } + public boolean remove(Object o) { return writeOperation(() -> cache.values().remove(o)); } + public boolean containsAll(Collection c) { return readOperation(() -> cache.values().containsAll(c)); } + public boolean addAll(Collection c) { throw new UnsupportedOperationException("addAll() not supported on values() of a Map"); } + public boolean removeAll(Collection c) { return writeOperation(() -> cache.values().removeAll(c)); } + public boolean retainAll(Collection c) { return writeOperation(() -> cache.values().retainAll(c)); } + public void clear() { writeOperation(() -> { cache.clear(); return null; }); } + }); } public Set> entrySet() { + return readOperation(() -> new Set>() { + public int size() { return readOperation(cache::size); } + public boolean isEmpty() { return readOperation(cache::isEmpty); } + public boolean contains(Object o) { return readOperation(() -> cache.entrySet().contains(o)); } + public Iterator> iterator() { + return new Iterator>() { + private final Iterator> it = cache.entrySet().iterator(); + private Entry current = (Entry) NO_ENTRY; + + public boolean hasNext() { return readOperation(it::hasNext); } + public Entry next() { return readOperation(() -> { current = it.next(); return current; }); } + public void remove() { + writeOperation(() -> { + if (current == NO_ENTRY) { + throw new IllegalStateException("Next not called or already removed"); + } + it.remove(); + current = (Entry) NO_ENTRY; + return null; + }); + } + }; + } + + public Object[] toArray() { return readOperation(() -> cache.entrySet().toArray()); } + public T[] toArray(T[] a) { return readOperation(() -> cache.entrySet().toArray(a)); } + public boolean add(Entry kvEntry) { throw new UnsupportedOperationException("add() not supported on entrySet() of a Map"); } + public boolean remove(Object o) { return writeOperation(() -> cache.entrySet().remove(o)); } + public boolean containsAll(Collection c) { return readOperation(() -> cache.entrySet().containsAll(c)); } + public boolean addAll(Collection> c) { throw new UnsupportedOperationException("addAll() not supported on entrySet() of a Map"); } + public boolean retainAll(Collection c) { return writeOperation(() -> cache.entrySet().retainAll(c)); } + public boolean removeAll(Collection c) { return writeOperation(() -> cache.entrySet().removeAll(c)); } + public void clear() { writeOperation(() -> { cache.clear(); return null; }); } + }); + } + + private T readOperation(Supplier operation) { lock.readLock().lock(); try { - return cache.entrySet(); + return operation.get(); } finally { lock.readLock().unlock(); } } - public V putIfAbsent(K key, V value) { + private T writeOperation(Supplier operation) { lock.writeLock().lock(); try { - V existingValue = cache.get(key); - if (existingValue == null) { - cache.put(key, value); - return null; - } - return existingValue; + return operation.get(); } finally { lock.writeLock().unlock(); } diff --git a/src/main/java/com/cedarsoftware/util/SealableList.java b/src/main/java/com/cedarsoftware/util/SealableList.java index aee98161..595b6951 100644 --- a/src/main/java/com/cedarsoftware/util/SealableList.java +++ b/src/main/java/com/cedarsoftware/util/SealableList.java @@ -33,7 +33,7 @@ */ public class SealableList implements List { private final List list; - private final Supplier sealedSupplier; + private final transient Supplier sealedSupplier; /** * Create a SealableList. Since no List is being supplied, this will use an ConcurrentList internally. If you diff --git a/src/main/java/com/cedarsoftware/util/SealableMap.java b/src/main/java/com/cedarsoftware/util/SealableMap.java index b0a62fef..28ccffc8 100644 --- a/src/main/java/com/cedarsoftware/util/SealableMap.java +++ b/src/main/java/com/cedarsoftware/util/SealableMap.java @@ -33,7 +33,7 @@ */ public class SealableMap implements Map { private final Map map; - private final Supplier sealedSupplier; + private final transient Supplier sealedSupplier; /** * Create a SealableMap. Since a Map is not supplied, this will use a ConcurrentHashMap internally. If you diff --git a/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java b/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java index cc7b8eb3..d8a3856f 100644 --- a/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java +++ b/src/main/java/com/cedarsoftware/util/SealableNavigableMap.java @@ -37,7 +37,7 @@ */ public class SealableNavigableMap implements NavigableMap { private final NavigableMap navMap; - private final Supplier sealedSupplier; + private final transient Supplier sealedSupplier; /** * Create a SealableNavigableMap. Since a Map is not supplied, this will use a ConcurrentSkipListMap internally. diff --git a/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java b/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java index 13af86c0..4ee44027 100644 --- a/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java +++ b/src/main/java/com/cedarsoftware/util/SealableNavigableSet.java @@ -36,7 +36,7 @@ */ public class SealableNavigableSet implements NavigableSet { private final NavigableSet navSet; - private final Supplier sealedSupplier; + private final transient Supplier sealedSupplier; /** * Create a NavigableSealableSet. Since a NavigableSet is not supplied, this will use a ConcurrentSkipListSet diff --git a/src/main/java/com/cedarsoftware/util/SealableSet.java b/src/main/java/com/cedarsoftware/util/SealableSet.java index 2030b9c8..f194b079 100644 --- a/src/main/java/com/cedarsoftware/util/SealableSet.java +++ b/src/main/java/com/cedarsoftware/util/SealableSet.java @@ -32,7 +32,7 @@ */ public class SealableSet implements Set { private final Set set; - private final Supplier sealedSupplier; + private final transient Supplier sealedSupplier; /** * Create a SealableSet. Since a Set is not supplied, this will use a ConcurrentHashMap.newKeySet internally. diff --git a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java b/src/test/java/com/cedarsoftware/util/LRUCacheTest.java index fd4c4961..2b0cd714 100644 --- a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java +++ b/src/test/java/com/cedarsoftware/util/LRUCacheTest.java @@ -1,15 +1,17 @@ package com.cedarsoftware.util; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - +import java.security.SecureRandom; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Set; +import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -145,22 +147,45 @@ void testPutIfAbsent() { @Test void testConcurrency() throws InterruptedException { ExecutorService service = Executors.newFixedThreadPool(3); + lruCache = new LRUCache<>(100000); // Perform a mix of put and get operations from multiple threads - for (int i = 0; i < 10000; i++) { - final int key = i % 3; // Keys will be 0, 1, 2 - final String value = "Value" + i; + int max = 10000; + int attempts = 0; + Random random = new SecureRandom(); + while (attempts++ < max) { + final int key = random.nextInt(max); + final String value = "V" + key; service.submit(() -> lruCache.put(key, value)); service.submit(() -> lruCache.get(key)); + service.submit(() -> lruCache.size()); + service.submit(() -> { + lruCache.keySet().remove(random.nextInt(max)); + }); + service.submit(() -> { + lruCache.values().remove("V" + random.nextInt(max)); + }); + final int attemptsCopy = attempts; + service.submit(() -> { + Iterator i = lruCache.entrySet().iterator(); + int walk = random.nextInt(attemptsCopy); + while (i.hasNext() && walk-- > 0) { + i.next(); + } + int chunk = 10; + while (i.hasNext() && chunk-- > 0) { + i.remove(); + i.next(); + } + }); + service.submit(() -> lruCache.remove(random.nextInt(max))); } service.shutdown(); assertTrue(service.awaitTermination(1, TimeUnit.MINUTES)); - - // Assert the final state of the cache - assertEquals(3, lruCache.size()); - Set keys = lruCache.keySet(); - assertTrue(keys.contains(0) || keys.contains(1) || keys.contains(2)); +// System.out.println("lruCache = " + lruCache); +// System.out.println("lruCache = " + lruCache.size()); +// System.out.println("attempts =" + attempts); } }