Skip to content

Commit df3c1de

Browse files
committed
MP-11740 Introduce Long2ShortTreeMap
1 parent 9d38702 commit df3c1de

12 files changed

Lines changed: 1009 additions & 0 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.trivago.fastutilconcurrentwrapper;
2+
import com.trivago.fastutilconcurrentwrapper.map.ConcurrentBusyWaitingLongShortTreeMap;
3+
import com.trivago.fastutilconcurrentwrapper.map.ConcurrentLongShortTreeMap;
4+
public final class ConcurrentLongShortTreeMapBuilder {
5+
private MapMode mapMode = MapMode.BLOCKING;
6+
private int buckets = 8;
7+
private short defaultValue = LongShortMap.DEFAULT_VALUE;
8+
private ConcurrentLongShortTreeMapBuilder() {
9+
}
10+
public static ConcurrentLongShortTreeMapBuilder newBuilder() {
11+
return new ConcurrentLongShortTreeMapBuilder();
12+
}
13+
public ConcurrentLongShortTreeMapBuilder withBuckets(int buckets) {
14+
this.buckets = buckets;
15+
return this;
16+
}
17+
public ConcurrentLongShortTreeMapBuilder withMode(MapMode mapMode) {
18+
this.mapMode = mapMode;
19+
return this;
20+
}
21+
public ConcurrentLongShortTreeMapBuilder withDefaultValue(short defaultValue) {
22+
this.defaultValue = defaultValue;
23+
return this;
24+
}
25+
public LongShortTreeMap build() {
26+
return mapMode.createMap(this);
27+
}
28+
public enum MapMode {
29+
BUSY_WAITING {
30+
@Override
31+
LongShortTreeMap createMap(ConcurrentLongShortTreeMapBuilder builder) {
32+
return new ConcurrentBusyWaitingLongShortTreeMap(
33+
builder.buckets,
34+
builder.defaultValue);
35+
}
36+
},
37+
BLOCKING {
38+
@Override
39+
LongShortTreeMap createMap(ConcurrentLongShortTreeMapBuilder builder) {
40+
return new ConcurrentLongShortTreeMap(
41+
builder.buckets,
42+
builder.defaultValue);
43+
}
44+
};
45+
abstract LongShortTreeMap createMap(ConcurrentLongShortTreeMapBuilder builder);
46+
}
47+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.trivago.fastutilconcurrentwrapper;
2+
import it.unimi.dsi.fastutil.longs.Long2ShortMap;
3+
/**
4+
* A sorted {@link LongShortMap} that additionally supports a {@code floorEntry()} operation.
5+
*/
6+
public interface LongShortTreeMap extends LongShortMap {
7+
/**
8+
* Returns the entry with the greatest key less than or equal to the given key,
9+
* or {@code null} if no such key exists.
10+
*
11+
* @param key the key to search for
12+
* @return an entry with the greatest key ≤ {@code key}, or {@code null}
13+
*/
14+
Long2ShortMap.Entry floorEntry(long key);
15+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.trivago.fastutilconcurrentwrapper.impl.longs;
2+
3+
import it.unimi.dsi.fastutil.longs.Long2ShortAVLTreeMap;
4+
import it.unimi.dsi.fastutil.longs.Long2ShortMap;
5+
import it.unimi.dsi.fastutil.longs.Long2ShortSortedMap;
6+
7+
/**
8+
* Extends {@link Long2ShortAVLTreeMap} with a {@code floorEntry()} method.
9+
*
10+
* <p>Fastutil's sorted maps do not provide NavigableMap-style floor/ceiling
11+
* operations. This class implements {@code floorEntry()} using the
12+
* {@link Long2ShortSortedMap#headMap(long)} workaround:
13+
*
14+
* <pre>
15+
* headMap(key + 1) → view of all keys &lt; (key + 1), i.e., keys ≤ key
16+
* lastLongKey() → largest key in that view = floor(key)
17+
* </pre>
18+
*
19+
* <p>The {@code headMap()} call returns a <em>view</em>, not a copy, so memory
20+
* overhead is O(1). The additional method-call frame adds negligible latency
21+
* (~1 ns) compared to the O(log n) tree traversal (~100–500 ns at 10M entries).
22+
*
23+
* <p>This class is placed in the {@code it.unimi.dsi.fastutil.longs} package
24+
* to allow future access to package-private tree internals if a more optimized
25+
* implementation becomes necessary.
26+
*
27+
* @see Long2ShortAVLTreeMap
28+
* @see Long2ShortSortedMap#headMap(long)
29+
*/
30+
public class FloorableLongShortAVLTreeMap extends Long2ShortAVLTreeMap {
31+
32+
private static final long serialVersionUID = 1L;
33+
34+
/**
35+
* Returns the entry with the greatest key less than or equal to the given key,
36+
* or {@code null} if no such key exists.
37+
*
38+
* <p>Implementation uses {@code headMap(key + 1).lastLongKey()} to find the
39+
* floor key, then constructs an entry with that key and its associated value.
40+
*
41+
* <p><b>Edge cases:</b>
42+
* <ul>
43+
* <li>If {@code key} equals {@link Long#MAX_VALUE}, {@code key + 1} overflows
44+
* to {@link Long#MIN_VALUE}. In this case we fall back to checking the
45+
* entire map's last key.</li>
46+
* <li>If the map is empty or all keys are greater than {@code key}, returns
47+
* {@code null}.</li>
48+
* </ul>
49+
*
50+
* @param key the key to search for
51+
* @return an entry with the greatest key ≤ {@code key}, or {@code null}
52+
*/
53+
public Long2ShortMap.Entry floorEntry(long key) {
54+
if (isEmpty()) {
55+
return null;
56+
}
57+
58+
// Handle Long.MAX_VALUE overflow: key + 1 wraps to Long.MIN_VALUE
59+
if (key == Long.MAX_VALUE) {
60+
// If key is MAX_VALUE, floor is the map's last key (if it exists)
61+
long lastKey = lastLongKey();
62+
return new BasicEntry(lastKey, get(lastKey));
63+
}
64+
65+
// headMap(toKey) returns keys strictly < toKey
66+
// So headMap(key + 1) returns keys ≤ key
67+
Long2ShortSortedMap head = headMap(key + 1);
68+
69+
if (head.isEmpty()) {
70+
return null;
71+
}
72+
73+
long floorKey = head.lastLongKey();
74+
return new BasicEntry(floorKey, get(floorKey));
75+
}
76+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package com.trivago.fastutilconcurrentwrapper.map;
2+
3+
import com.trivago.fastutilconcurrentwrapper.LongShortTreeMap;
4+
import com.trivago.fastutilconcurrentwrapper.wrapper.PrimitiveFastutilLongShortTreeWrapper;
5+
import it.unimi.dsi.fastutil.longs.Long2ShortFunction;
6+
import it.unimi.dsi.fastutil.longs.Long2ShortMap;
7+
8+
import java.util.concurrent.locks.Lock;
9+
import java.util.function.BiFunction;
10+
11+
public class ConcurrentBusyWaitingLongShortTreeMap extends PrimitiveConcurrentMap implements LongShortTreeMap {
12+
13+
private final LongShortTreeMap[] maps;
14+
private final short defaultValue;
15+
16+
public ConcurrentBusyWaitingLongShortTreeMap(int numBuckets,
17+
short defaultValue) {
18+
super(numBuckets);
19+
20+
this.maps = new LongShortTreeMap[numBuckets];
21+
this.defaultValue = defaultValue;
22+
23+
for (int i = 0; i < numBuckets; i++) {
24+
maps[i] = new PrimitiveFastutilLongShortTreeWrapper(defaultValue);
25+
}
26+
}
27+
28+
@Override
29+
public int size() {
30+
return super.size(maps);
31+
}
32+
33+
@Override
34+
public boolean isEmpty() {
35+
return super.isEmpty(maps);
36+
}
37+
38+
@Override
39+
public boolean containsKey(long key) {
40+
int bucket = getBucket(key);
41+
42+
Lock readLock = locks[bucket].readLock();
43+
44+
while (true) {
45+
if (readLock.tryLock()) {
46+
try {
47+
return maps[bucket].containsKey(key);
48+
} finally {
49+
readLock.unlock();
50+
}
51+
}
52+
}
53+
}
54+
@Override
55+
public Long2ShortMap.Entry floorEntry(long key) {
56+
int bucket = getBucket(key);
57+
Lock readLock = locks[bucket].readLock();
58+
while (true) {
59+
if (readLock.tryLock()) {
60+
try {
61+
return maps[bucket].floorEntry(key);
62+
} finally {
63+
readLock.unlock();
64+
}
65+
}
66+
}
67+
}
68+
69+
@Override
70+
public short get(long key) {
71+
int bucket = getBucket(key);
72+
73+
Lock readLock = locks[bucket].readLock();
74+
75+
while (true) {
76+
if (readLock.tryLock()) {
77+
try {
78+
return maps[bucket].get(key);
79+
} finally {
80+
readLock.unlock();
81+
}
82+
}
83+
}
84+
}
85+
86+
@Override
87+
public short put(long key, short value) {
88+
int bucket = getBucket(key);
89+
90+
Lock writeLock = locks[bucket].writeLock();
91+
92+
while (true) {
93+
if (writeLock.tryLock()) {
94+
try {
95+
return maps[bucket].put(key, value);
96+
} finally {
97+
writeLock.unlock();
98+
}
99+
}
100+
}
101+
}
102+
103+
@Override
104+
public short getDefaultValue() {
105+
return defaultValue;
106+
}
107+
108+
@Override
109+
public short remove(long key) {
110+
int bucket = getBucket(key);
111+
112+
Lock writeLock = locks[bucket].writeLock();
113+
114+
while (true) {
115+
if (writeLock.tryLock()) {
116+
try {
117+
return maps[bucket].remove(key);
118+
} finally {
119+
writeLock.unlock();
120+
}
121+
}
122+
}
123+
}
124+
125+
@Override
126+
public boolean remove(long key, short value) {
127+
int bucket = getBucket(key);
128+
129+
Lock writeLock = locks[bucket].writeLock();
130+
131+
while (true) {
132+
if (writeLock.tryLock()) {
133+
try {
134+
return maps[bucket].remove(key, value);
135+
} finally {
136+
writeLock.unlock();
137+
}
138+
}
139+
}
140+
}
141+
142+
@Override
143+
public short computeIfAbsent(long key, Long2ShortFunction mappingFunction) {
144+
int bucket = getBucket(key);
145+
146+
Lock writeLock = locks[bucket].writeLock();
147+
148+
while (true) {
149+
if (writeLock.tryLock()) {
150+
try {
151+
return maps[bucket].computeIfAbsent(key, mappingFunction);
152+
} finally {
153+
writeLock.unlock();
154+
}
155+
}
156+
}
157+
}
158+
159+
@Override
160+
public short computeIfPresent(long key, BiFunction<Long, Short, Short> mappingFunction) {
161+
int bucket = getBucket(key);
162+
163+
Lock writeLock = locks[bucket].writeLock();
164+
165+
while (true) {
166+
if (writeLock.tryLock()) {
167+
try {
168+
return maps[bucket].computeIfPresent(key, mappingFunction);
169+
} finally {
170+
writeLock.unlock();
171+
}
172+
}
173+
}
174+
}
175+
}
176+
177+
178+

0 commit comments

Comments
 (0)