001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.util.concurrency; 020 021import java.lang.ref.ReferenceQueue; 022import java.lang.ref.WeakReference; 023import java.util.concurrent.ConcurrentHashMap; 024 025/** 026 * A concurrent cache with weak keys and weak values, inspired by maven-impl's Cache class 027 * but stripped down and optimized for hot-path performance. 028 * <p> 029 * Design compared to the full Cache: 030 * <ul> 031 * <li><b>Zero allocation on get()</b> — uses a ThreadLocal reusable lookup key 032 * instead of allocating a new wrapper on every read</li> 033 * <li><b>O(1) cleanup per stale entry</b> — uses identity-based removal from 034 * the ReferenceQueue instead of a full {@code entrySet().removeIf()} scan</li> 035 * <li><b>Lock-free reads</b> — {@link ConcurrentHashMap#get} is a volatile read 036 * with no lock acquisition</li> 037 * <li><b>Lock-striped writes</b> — ConcurrentHashMap uses fine-grained locking</li> 038 * </ul> 039 * <p> 040 * Keys are held via {@link WeakReference}: when a key is no longer strongly referenced 041 * elsewhere, its entry becomes eligible for garbage collection. Values are also held 042 * via WeakReference. Stale entries are cleaned up lazily on {@link #put}. 043 * 044 * @param <K> the type of keys 045 * @param <V> the type of values 046 * @since 2.0.19 047 */ 048public class ConcurrentWeakCache<K, V> { 049 050 private final ConcurrentHashMap<Object, WeakReference<V>> map; 051 private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); 052 053 @SuppressWarnings("rawtypes") 054 private static final ThreadLocal<LookupKey> LOOKUP_KEY = new ThreadLocal<LookupKey>() { 055 @Override 056 protected LookupKey initialValue() { 057 return new LookupKey(); 058 } 059 }; 060 061 /** 062 * Creates a new cache with default initial capacity. 063 */ 064 public ConcurrentWeakCache() { 065 this.map = new ConcurrentHashMap<>(); 066 } 067 068 /** 069 * Creates a new cache with the given initial capacity. 070 * 071 * @param initialCapacity the initial capacity 072 */ 073 public ConcurrentWeakCache(int initialCapacity) { 074 this.map = new ConcurrentHashMap<>(initialCapacity); 075 } 076 077 /** 078 * Returns the value for the given key, or {@code null} if the key is not present 079 * or the value has been garbage collected. 080 * <p> 081 * This method is lock-free and allocates no objects. 082 * 083 * @param key the key to look up 084 * @return the cached value, or {@code null} 085 */ 086 @SuppressWarnings("unchecked") 087 public V get(K key) { 088 LookupKey<K> lookupKey = LOOKUP_KEY.get(); 089 lookupKey.set(key); 090 try { 091 WeakReference<V> ref = map.get(lookupKey); 092 return ref != null ? ref.get() : null; 093 } finally { 094 lookupKey.clear(); 095 } 096 } 097 098 /** 099 * 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}