1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.util.concurrency;
20
21 import java.lang.ref.ReferenceQueue;
22 import java.lang.ref.WeakReference;
23 import java.util.concurrent.ConcurrentHashMap;
24
25 /**
26 * A concurrent cache with weak keys and weak values, inspired by maven-impl's Cache class
27 * but stripped down and optimized for hot-path performance.
28 * <p>
29 * Design compared to the full Cache:
30 * <ul>
31 * <li><b>Zero allocation on get()</b> — uses a ThreadLocal reusable lookup key
32 * instead of allocating a new wrapper on every read</li>
33 * <li><b>O(1) cleanup per stale entry</b> — uses identity-based removal from
34 * the ReferenceQueue instead of a full {@code entrySet().removeIf()} scan</li>
35 * <li><b>Lock-free reads</b> — {@link ConcurrentHashMap#get} is a volatile read
36 * with no lock acquisition</li>
37 * <li><b>Lock-striped writes</b> — ConcurrentHashMap uses fine-grained locking</li>
38 * </ul>
39 * <p>
40 * Keys are held via {@link WeakReference}: when a key is no longer strongly referenced
41 * elsewhere, its entry becomes eligible for garbage collection. Values are also held
42 * via WeakReference. Stale entries are cleaned up lazily on {@link #put}.
43 *
44 * @param <K> the type of keys
45 * @param <V> the type of values
46 * @since 2.0.19
47 */
48 public class ConcurrentWeakCache<K, V> {
49
50 private final ConcurrentHashMap<Object, WeakReference<V>> map;
51 private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
52
53 @SuppressWarnings("rawtypes")
54 private static final ThreadLocal<LookupKey> LOOKUP_KEY = new ThreadLocal<LookupKey>() {
55 @Override
56 protected LookupKey initialValue() {
57 return new LookupKey();
58 }
59 };
60
61 /**
62 * Creates a new cache with default initial capacity.
63 */
64 public ConcurrentWeakCache() {
65 this.map = new ConcurrentHashMap<>();
66 }
67
68 /**
69 * Creates a new cache with the given initial capacity.
70 *
71 * @param initialCapacity the initial capacity
72 */
73 public ConcurrentWeakCache(int initialCapacity) {
74 this.map = new ConcurrentHashMap<>(initialCapacity);
75 }
76
77 /**
78 * Returns the value for the given key, or {@code null} if the key is not present
79 * or the value has been garbage collected.
80 * <p>
81 * This method is lock-free and allocates no objects.
82 *
83 * @param key the key to look up
84 * @return the cached value, or {@code null}
85 */
86 @SuppressWarnings("unchecked")
87 public V get(K key) {
88 LookupKey<K> lookupKey = LOOKUP_KEY.get();
89 lookupKey.set(key);
90 try {
91 WeakReference<V> ref = map.get(lookupKey);
92 return ref != null ? ref.get() : null;
93 } finally {
94 lookupKey.clear();
95 }
96 }
97
98 /**
99 * Stores a key-value pair in the cache. Both key and value are held via weak references.
100 * <p>
101 * Also performs lazy cleanup of entries whose keys have been garbage collected.
102 *
103 * @param key the key
104 * @param value the value
105 */
106 public void put(K key, V value) {
107 expungeStaleEntries();
108 map.put(new WeakKey<>(key, queue), new WeakReference<>(value));
109 }
110
111 /**
112 * If the key is not already present (or its value has been GC'd), stores the key-value pair
113 * and returns the given value. If the key is already present with a live value, returns the
114 * existing value without storing. Concurrent callers for the same key are guaranteed to
115 * receive the same instance (the insert-or-keep decision is atomic per key via
116 * {@link ConcurrentHashMap#merge}).
117 * <p>
118 * Also performs lazy cleanup of entries whose keys have been garbage collected.
119 *
120 * @param key the key
121 * @param value the value to store if absent
122 * @return the existing value if present, or the given value if newly stored
123 */
124 public V putIfAbsent(K key, V value) {
125 // Fast path: lock-free lookup, zero allocation (ThreadLocal LookupKey)
126 V existing = get(key);
127 if (existing != null) {
128 return existing;
129 }
130 // Slow path: atomic insert-or-keep via merge() — locks only the target bucket,
131 // so contention is per-key, not global. Handles the case where an existing entry's
132 // value has been GC'd by atomically replacing it with the new value.
133 expungeStaleEntries();
134 WeakReference<V> newRef = new WeakReference<>(value);
135 WeakReference<V> ref = map.merge(new WeakKey<>(key, queue), newRef, (existingRef, incoming) -> {
136 V existingValue = existingRef.get();
137 return existingValue != null ? existingRef : incoming;
138 });
139 if (ref != newRef) {
140 V existingValue = ref.get();
141 if (existingValue != null) {
142 return existingValue;
143 }
144 }
145 return value;
146 }
147
148 /**
149 * Returns the number of entries in the cache (including possibly stale ones).
150 *
151 * @return the cache size
152 */
153 public int size() {
154 return map.size();
155 }
156
157 /**
158 * Removes entries whose weak-reference keys have been garbage collected.
159 * Called automatically on {@link #put}. Each stale entry is removed in O(1)
160 * via identity match — no map scan required.
161 */
162 private void expungeStaleEntries() {
163 Object staleKey;
164 while ((staleKey = queue.poll()) != null) {
165 // ConcurrentHashMap.remove() checks (storedKey == staleKey) first.
166 // Since the staleKey IS the same WeakKey object that was stored in
167 // the map, identity matches and the entry is removed in O(1).
168 map.remove(staleKey);
169 }
170 }
171
172 /**
173 * Reusable lookup key stored in a ThreadLocal for zero-allocation reads.
174 * Used only for {@link ConcurrentHashMap#get} lookups, never stored in the map.
175 * <p>
176 * Implements equals/hashCode to match {@link WeakKey} entries in the map:
177 * ConcurrentHashMap.get(lookupKey) calls {@code lookupKey.equals(storedWeakKey)},
178 * which delegates to {@code key.equals(storedWeakKey.get())}.
179 */
180 static class LookupKey<K> {
181 private K key;
182 private int hash;
183
184 void set(K key) {
185 this.key = key;
186 this.hash = key.hashCode();
187 }
188
189 void clear() {
190 this.key = null; // prevent leak via ThreadLocal
191 }
192
193 @Override
194 public int hashCode() {
195 return hash;
196 }
197
198 @Override
199 @SuppressWarnings("rawtypes")
200 public boolean equals(Object o) {
201 if (o instanceof WeakKey) {
202 Object otherKey = ((WeakKey) o).get();
203 return key != null && key.equals(otherKey);
204 }
205 return false;
206 }
207 }
208
209 /**
210 * Weak-reference key stored in the map. When the referent is garbage collected,
211 * this object is enqueued in the {@link ReferenceQueue} for cleanup.
212 * <p>
213 * The cleanup path uses identity: {@code map.remove(staleWeakKey)} matches because
214 * ConcurrentHashMap checks {@code storedKey == staleWeakKey} before calling equals().
215 * Since the queued WeakKey IS the same object that's in the map, this is always true.
216 */
217 static class WeakKey<K> extends WeakReference<K> {
218 private final int hash;
219
220 @SuppressWarnings("unchecked")
221 WeakKey(K key, ReferenceQueue<? super K> queue) {
222 super(key, (ReferenceQueue<? super K>) queue);
223 this.hash = key.hashCode();
224 }
225
226 @Override
227 public int hashCode() {
228 return hash;
229 }
230
231 @Override
232 @SuppressWarnings("rawtypes")
233 public boolean equals(Object o) {
234 if (this == o) {
235 return true; // identity match — used for cleanup and normal dedup
236 }
237 K k = get();
238 if (k == null) {
239 return false; // referent GC'd
240 }
241 if (o instanceof WeakKey) {
242 return k.equals(((WeakKey) o).get());
243 }
244 if (o instanceof LookupKey) {
245 return k.equals(((LookupKey) o).key);
246 }
247 return false;
248 }
249 }
250 }