Skip to content

Commit b7a9bbf

Browse files
committed
StripedNonBlockingHashMapLong: better key iterable; + unmodifiable values()
1 parent 3e895ed commit b7a9bbf

File tree

2 files changed

+179
-34
lines changed

2 files changed

+179
-34
lines changed

src/main/java/com/trivago/fastutilconcurrentwrapper/longkey/StripedNonBlockingHashMapLong.java

Lines changed: 139 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import com.trivago.fastutilconcurrentwrapper.PrimitiveConcurrentMap;
44
import com.trivago.fastutilconcurrentwrapper.PrimitiveKeyMap;
55
import com.trivago.fastutilconcurrentwrapper.util.PaddedLock;
6+
import com.trivago.fastutilconcurrentwrapper.util.SmartIterator;
7+
import com.trivago.fastutilconcurrentwrapper.util.SmartLongIterator;
68
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
7-
import it.unimi.dsi.fastutil.longs.LongIterator;
89
import it.unimi.dsi.fastutil.longs.LongSet;
910
import it.unimi.dsi.fastutil.objects.ObjectCollection;
11+
import it.unimi.dsi.fastutil.objects.ObjectCollections;
1012
import it.unimi.dsi.fastutil.objects.ObjectSet;
1113
import org.jctools.maps.NonBlockingHashMapLong;
1214
import org.jspecify.annotations.Nullable;
1315

16+
import java.util.Collection;
1417
import java.util.Map;
1518
import java.util.concurrent.CancellationException;
1619
import java.util.concurrent.ConcurrentMap;
@@ -20,6 +23,8 @@
2023
import java.util.function.Consumer;
2124
import java.util.function.Function;
2225
import java.util.function.LongConsumer;
26+
import java.util.function.Predicate;
27+
import java.util.stream.Stream;
2328

2429
/**
2530
Similar to {@link ConcurrentLongObjectMap}, but backed with NonBlockingHashMapLong ⇒ non-blocking reads 🚀
@@ -33,7 +38,7 @@
3338
@see NBHMLCacheExpirer
3439
*/
3540
@SuppressWarnings("LockAcquiredButNotSafelyReleased")
36-
public class StripedNonBlockingHashMapLong<E> implements ConcurrentMap<Long,E>, Long2ObjectMap<E>, PrimitiveKeyMap {
41+
public class StripedNonBlockingHashMapLong<E> implements ConcurrentMap<Long,E>, Long2ObjectMap<E>, PrimitiveKeyMap, Iterable<Long> {
3742
final NonBlockingHashMapLong<E> m;
3843
/** @see com.google.common.util.concurrent.Striped#lock(int) */
3944
final PaddedLock[] s;
@@ -174,7 +179,7 @@ public void putAll (Map<? extends Long,? extends E> fromMap) {
174179
}
175180

176181
/** @see NonBlockingHashMapLong.IteratorLong */
177-
public static class StripedLongIterator implements LongIterator {
182+
public static class StripedLongIterator implements SmartLongIterator {
178183
private final StripedNonBlockingHashMapLong<?> owner;
179184
private final NonBlockingHashMapLong<?>.IteratorLong it;
180185
private long seenKey;// ^ safe for concurrent
@@ -191,8 +196,6 @@ public void remove () {
191196
it.remove();
192197
}
193198
}
194-
/** <strong>Auto-box</strong> and return the next key. */
195-
@Override @Deprecated public Long next (){ return nextLong(); }
196199
/** Return the next key as a primitive {@code long}. */
197200
@Override
198201
public long nextLong () {
@@ -203,7 +206,7 @@ public long nextLong () {
203206
@Override public boolean hasNext (){ return it.hasNext(); }
204207
}//StripedLongIterator
205208

206-
public StripedLongIterator keys (){ return new StripedLongIterator(this); }
209+
@Override public StripedLongIterator iterator (){ return new StripedLongIterator(this); }
207210

208211
public void forEachKey (LongConsumer action) {
209212
var it = (NonBlockingHashMapLong<E>.IteratorLong) m.keys();
@@ -219,10 +222,110 @@ public LongSet keySet () {
219222
}
220223
public long[] keySetLong (){ return m.keySetLong(); }
221224

222-
/** @see NonBlockingHashMapLong#values() */
225+
/**
226+
@see NonBlockingHashMapLong#values()
227+
@see ObjectCollections#unmodifiable(ObjectCollection)
228+
*/
223229
@Override
224230
public ObjectCollection<E> values () {
225-
throw new UnsupportedOperationException();
231+
var collection = m.values();
232+
return new ObjectCollection<>(){
233+
@Override
234+
public boolean add (E k) {
235+
throw new UnsupportedOperationException();
236+
}
237+
@Override
238+
public boolean remove (Object k) {
239+
throw new UnsupportedOperationException();
240+
}
241+
@Override
242+
public int size () {
243+
return collection.size();
244+
}
245+
@Override
246+
public boolean isEmpty () {
247+
return collection.isEmpty();
248+
}
249+
@Override
250+
public boolean contains (Object o) {
251+
return collection.contains(o);
252+
}
253+
254+
@Override
255+
public SmartIterator<E> iterator() {
256+
var it = collection.iterator();
257+
return new SmartIterator<>(){
258+
@Override public boolean hasNext (){ return it.hasNext(); }
259+
@Override public E next (){ return it.next(); }
260+
@Override public String toString (){ return it.toString(); }
261+
};
262+
}
263+
264+
@Override public SmartIterator<E> spliterator (){ return iterator(); }
265+
@Override public Stream<E> stream (){ return iterator().stream(); }
266+
@Override public Stream<E> parallelStream (){ return iterator().stream().parallel(); }
267+
268+
@Override
269+
public void clear() {
270+
throw new UnsupportedOperationException();
271+
}
272+
273+
@Override
274+
public <T> T[] toArray (T[] a) {
275+
return collection.toArray(a);
276+
}
277+
278+
@Override
279+
public Object[] toArray() {
280+
return collection.toArray();
281+
}
282+
283+
@Override
284+
public void forEach(final Consumer<? super E> action) {
285+
collection.forEach(action);
286+
}
287+
288+
@Override
289+
public boolean containsAll(Collection<?> c) {
290+
return collection.containsAll(c);
291+
}
292+
293+
@Override
294+
public boolean addAll(Collection<? extends E> c) {
295+
throw new UnsupportedOperationException();
296+
}
297+
298+
@Override
299+
public boolean removeAll(Collection<?> c) {
300+
throw new UnsupportedOperationException();
301+
}
302+
303+
@Override
304+
public boolean retainAll(Collection<?> c) {
305+
throw new UnsupportedOperationException();
306+
}
307+
308+
@Override
309+
public boolean removeIf (Predicate<? super E> filter) {
310+
throw new UnsupportedOperationException();
311+
}
312+
313+
@Override
314+
public String toString() {
315+
return collection.toString();
316+
}
317+
318+
@Override
319+
public int hashCode() {
320+
return collection.hashCode();
321+
}
322+
323+
@Override
324+
public boolean equals (Object o) {
325+
if (o == this) return true;
326+
return collection.equals(o);
327+
}
328+
};
226329
}
227330

228331
@Override
@@ -304,8 +407,8 @@ public <R> R withLock (long key, Function<Long2ObjectMap.Entry<E>,R> withLock) {
304407
}
305408
}
306409

307-
308-
public abstract static class NBHMLCacheExpirer<E> implements LongConsumer, Function<Long2ObjectMap.Entry<E>,Object>{
410+
/** Simple template to add an expiration support */
411+
public abstract static class NBHMLCacheExpirer<E> {
309412
protected final StripedNonBlockingHashMapLong<E> cacheMap;
310413

311414
public NBHMLCacheExpirer (StripedNonBlockingHashMapLong<E> cacheMap){ this.cacheMap = cacheMap; }//new
@@ -333,35 +436,37 @@ protected void beforeExpire () {
333436
334437
@see org.springframework.scheduling.annotation.Scheduled
335438
@see java.util.Set#removeIf
439+
440+
@see StripedNonBlockingHashMapLong#forEachKey(LongConsumer)
441+
@see StripedNonBlockingHashMapLong#withLock
336442
*/
337-
public void expire () {
443+
public long expire () {
338444
long initialExpiredCount = expiredCount;
339445
beforeExpire();
340-
cacheMap.forEachKey(this);// see accept(long longKey)
341-
reportExpireResult(initialExpiredCount);
446+
try {
447+
for (var it = (NonBlockingHashMapLong<E>.IteratorLong) cacheMap.m.keys(); it.hasNext();){
448+
long longKey = it.nextLong();
449+
E value = cacheMap.get(longKey);
450+
if (value != null && isExpired(longKey, value)){// first "light" check: value could be gone already #1
451+
452+
try (var __ = cacheMap.write(longKey)){
453+
value = cacheMap.get(longKey);// can be gone already #2
454+
if (value != null && isExpired(longKey, value)){// double check idiom
455+
expiredCount++;
456+
postProcessExpiredEntry(longKey, value);
457+
it.remove();// expired ⇒ remove: we inside the write lock ⇒ allowed
458+
}
459+
}
460+
461+
}//#1
462+
}//f keys
463+
} catch (CancellationException ignored){}
464+
return afterExpire(initialExpiredCount);
342465
}
343-
344-
/** @see #forEachKey(LongConsumer) */
345-
@Override
346-
public void accept (long longKey) {
347-
E value = cacheMap.get(longKey);// can be gone already
348-
if (value != null && isExpired(longKey, value))// first "light" check
349-
cacheMap.withLock(longKey, this);
350-
}//LongConsumer#accept for expire → forEachKey
351-
352-
/** @see #withLock(long, Function) */
353-
@Override
354-
public @Nullable Object apply (Long2ObjectMap.Entry<E> e) {
355-
if (e.getValue() != null && isExpired(e.getLongKey(), e.getValue())){// double check idiom
356-
expiredCount++;
357-
postProcessExpiredEntry(e.getLongKey(), e.getValue());
358-
e.setValue(null);// expired ⇒ remove
359-
}
360-
return null;
361-
}//Function#apply for LongConsumer#accept → withLock
362-
363-
private void reportExpireResult (long initialExpiredCount) {
466+
/** e.g. reportExpireResult */
467+
protected long afterExpire (long initialExpiredCount) {
364468
//LOGGER.debug("{}.expire entries: {} / {}, expiredSinceStart: {}", beanName, expiredCount - initialExpiredCount, cacheMap.size(), expiredCount);
469+
return expiredCount - initialExpiredCount;
365470
}
366471
}
367472
}

src/test/java/com/trivago/fastutilconcurrentwrapper/longkey/StripedNonBlockingHashMapLongTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.concurrent.ThreadLocalRandom;
1111
import java.util.concurrent.TimeUnit;
1212
import java.util.concurrent.atomic.AtomicInteger;
13+
import java.util.concurrent.atomic.AtomicLong;
14+
import java.util.stream.Collectors;
1315

1416
import static org.junit.jupiter.api.Assertions.*;
1517

@@ -273,4 +275,42 @@ void _withLock () {
273275
assertNull(map.get(2));
274276
assertFalse(map.containsKey(2));
275277
}
278+
279+
@Test
280+
void _expire () {
281+
var now = new AtomicLong();
282+
var cache = new StripedNonBlockingHashMapLong<String>(100, true, 8);
283+
var expirer = new StripedNonBlockingHashMapLong.NBHMLCacheExpirer<>(cache){
284+
@Override
285+
protected boolean isExpired (long key, String value) {
286+
return key < now.get();
287+
}
288+
};
289+
290+
cache.put(1, "One");
291+
cache.put(2, "Two");
292+
cache.put(3, "Three");
293+
cache.put(4, "Four");
294+
cache.put(5, "Five");
295+
assertEquals(5, cache.size());
296+
297+
assertEquals(0, expirer.expire());
298+
assertEquals(5, cache.size());
299+
assertEquals("One, Two, Three, Four, Five", cache.iterator().stream().sorted().map(cache::get).collect(Collectors.joining(", ")));
300+
301+
now.set(2);
302+
assertEquals(1, expirer.expire());
303+
assertEquals(4, cache.size());
304+
assertEquals("Two, Three, Four, Five", cache.iterator().stream().sorted().map(cache::get).collect(Collectors.joining(", ")));
305+
306+
now.set(4);
307+
assertEquals(2, expirer.expire());
308+
assertEquals(2, cache.size());
309+
assertEquals("Four, Five", cache.iterator().stream().sorted().map(cache::get).collect(Collectors.joining(", ")));
310+
311+
now.set(Long.MAX_VALUE);
312+
assertEquals(2, expirer.expire());
313+
assertEquals(0, cache.size());
314+
assertEquals("", cache.iterator().stream().sorted().map(cache::get).collect(Collectors.joining(", ")));
315+
}
276316
}

0 commit comments

Comments
 (0)