2020
2121import java .util .Objects ;
2222import java .util .concurrent .ConcurrentHashMap ;
23- import java .util .concurrent .atomic .AtomicBoolean ;
23+ import java .util .concurrent .atomic .AtomicReference ;
2424
2525import com .google .common .annotations .VisibleForTesting ;
2626import org .slf4j .Logger ;
@@ -42,8 +42,7 @@ public class ZstdCompressionDictionary implements CompressionDictionary, SelfRef
4242 private final int checksum ;
4343 // One ZstdDictDecompress and multiple ZstdDictCompress (per level) can be derived from the same raw dictionary content
4444 private final ConcurrentHashMap <Integer , ZstdDictCompress > zstdDictCompressPerLevel = new ConcurrentHashMap <>();
45- private volatile ZstdDictDecompress dictDecompress ;
46- private final AtomicBoolean closed = new AtomicBoolean (false );
45+ private final AtomicReference <ZstdDictDecompress > dictDecompress = new AtomicReference <>();
4746 private final Ref <ZstdCompressionDictionary > selfRef ;
4847
4948 @ VisibleForTesting
@@ -90,7 +89,7 @@ public int checksum()
9089 public int estimatedOccupiedMemoryBytes ()
9190 {
9291 int occupied = rawDictionary .length ;
93- occupied += dictDecompress != null ? rawDictionary .length : 0 ;
92+ occupied += dictDecompress . get () != null ? rawDictionary .length : 0 ;
9493 occupied += zstdDictCompressPerLevel .size () * rawDictionary .length ;
9594
9695 return occupied ;
@@ -114,50 +113,65 @@ public int hashCode()
114113 * Get a pre-processed compression tables that is optimized for compression.
115114 * It is derived/computed from dictionary bytes.
116115 * The internal data structure is different from the tables for decompression.
117- *
116+ * <br>
117+ * IMPORTANT: Caller MUST hold a valid reference (via tryRef/ref) to this dictionary.
118+ * The reference counting mechanism ensures tidy() cannot run while references exist,
119+ * making synchronization unnecessary. This method is safe to call concurrently as long
120+ * as each caller holds a reference.
121+ * <br>
118122 * @param compressionLevel compression level to create the compression table
119- * @return ZstdDictCompress
123+ * @return ZstdDictCompress for the specified compression level
124+ * @throws IllegalStateException if called without holding a valid reference
120125 */
121126 public ZstdDictCompress dictionaryForCompression (int compressionLevel )
122127 {
123- if (closed .get ())
124- throw new IllegalStateException ("Dictionary has been closed. " + dictId );
125-
128+ ensureNotReleased ();
126129 ZstdCompressorBase .validateCompressionLevel (compressionLevel );
127130
128- return zstdDictCompressPerLevel .computeIfAbsent (compressionLevel , level -> {
129- if (closed .get ())
130- throw new IllegalStateException ("Dictionary has been closed" );
131- return new ZstdDictCompress (rawDictionary , level );
132- });
131+ // Fast path: check if already exists to avoid locking the bin
132+ ZstdDictCompress existing = zstdDictCompressPerLevel .get (compressionLevel );
133+ if (existing != null )
134+ return existing ;
135+
136+ // A little slow path: create new dictionary for this compression level
137+ // No additional synchronization needed - reference counting prevents tidy() while in use
138+ return zstdDictCompressPerLevel .computeIfAbsent (compressionLevel , level ->
139+ new ZstdDictCompress (rawDictionary , level ));
133140 }
134141
135142 /**
136143 * Get a pre-processed decompression tables that is optimized for decompression.
137144 * It is derived/computed from dictionary bytes.
138145 * The internal data structure is different from the tables for compression.
146+ * <br>
147+ * IMPORTANT: Caller MUST hold a valid reference (via tryRef/ref) to this dictionary.
148+ * The reference counting mechanism ensures tidy() cannot run while references exist,
149+ * making synchronization unnecessary. This method is safe to call concurrently as long
150+ * as each caller holds a reference.
151+ * <br>
152+ * Thread-safe: Multiple threads can safely call this method concurrently.
153+ * The decompression dictionary will be created exactly once on first access.
139154 *
140- * @return ZstdDictDecompress
155+ * @return ZstdDictDecompress for decompression operations
156+ * @throws IllegalStateException if called without holding a valid reference
141157 */
142158 public ZstdDictDecompress dictionaryForDecompression ()
143159 {
144- if (closed .get ())
145- throw new IllegalStateException ("Dictionary has been closed" );
146-
147- ZstdDictDecompress result = dictDecompress ;
160+ ensureNotReleased ();
161+ // Fast path: if already initialized, return immediately
162+ ZstdDictDecompress result = dictDecompress .get ();
148163 if (result != null )
149164 return result ;
150165
166+ // Slow path: need to initialize with proper double-checked locking
167+ // Reference counting guarantees tidy() won't run during this operation
151168 synchronized (this )
152169 {
153- if (closed .get ())
154- throw new IllegalStateException ("Dictionary has been closed" );
155-
156- result = dictDecompress ;
170+ result = dictDecompress .get ();
157171 if (result == null )
158172 {
159173 result = new ZstdDictDecompress (rawDictionary );
160- dictDecompress = result ;
174+ dictDecompress . set ( result ) ;
161175 }
162176 return result ;
163177 }
@@ -181,30 +195,48 @@ public Ref<ZstdCompressionDictionary> ref()
181195 return selfRef .ref ();
182196 }
183197
184- @ Override
185- public void close ()
198+ private void ensureNotReleased ()
186199 {
187- if (closed .compareAndSet (false , true ))
188- {
189- selfRef .release ();
190- }
200+ if (selfRef .globalCount () <= 0 )
201+ throw new IllegalStateException ("Dictionary has been released: " + dictId );
191202 }
192203
204+ /**
205+ * Tidy implementation for cleaning up native Zstd resources.
206+ *
207+ * This class holds direct references to the resources that need cleanup,
208+ * avoiding a circular reference pattern where Tidy would hold a reference
209+ * to the parent dictionary object.
210+ */
193211 private static class Tidy implements RefCounted .Tidy
194212 {
195213 private final ConcurrentHashMap <Integer , ZstdDictCompress > zstdDictCompressPerLevel ;
196- private volatile ZstdDictDecompress dictDecompress ;
214+ private final AtomicReference < ZstdDictDecompress > dictDecompress ;
197215
198- Tidy (ConcurrentHashMap <Integer , ZstdDictCompress > zstdDictCompressPerLevel , ZstdDictDecompress dictDecompress )
216+ Tidy (ConcurrentHashMap <Integer , ZstdDictCompress > zstdDictCompressPerLevel ,
217+ AtomicReference <ZstdDictDecompress > dictDecompress )
199218 {
200219 this .zstdDictCompressPerLevel = zstdDictCompressPerLevel ;
201220 this .dictDecompress = dictDecompress ;
202221 }
203222
223+ /**
224+ * Clean up native resources when reference count reaches zero.
225+ *
226+ * IMPORTANT: This method is called exactly once when the last reference is released.
227+ * Reference counting guarantees that no other thread can be executing
228+ * dictionaryForCompression/Decompression when this runs, because:
229+ * 1. Those methods require holding a valid reference
230+ * 2. This only runs when refcount goes from 0 to -1
231+ * 3. Once refcount is negative, tryRef() returns null, preventing new references
232+ *
233+ * Therefore, no synchronization is needed - we have exclusive access to clean up.
234+ */
204235 @ Override
205236 public void tidy ()
206237 {
207238 // Close all compression dictionaries
239+ // No synchronization needed - reference counting ensures exclusive access
208240 for (ZstdDictCompress compressDict : zstdDictCompressPerLevel .values ())
209241 {
210242 try
@@ -220,7 +252,7 @@ public void tidy()
220252 zstdDictCompressPerLevel .clear ();
221253
222254 // Close decompression dictionary
223- ZstdDictDecompress decompressDict = dictDecompress ;
255+ ZstdDictDecompress decompressDict = dictDecompress . get () ;
224256 if (decompressDict != null )
225257 {
226258 try
@@ -231,7 +263,7 @@ public void tidy()
231263 {
232264 logger .warn ("Failed to close ZstdDictDecompress" , e );
233265 }
234- dictDecompress = null ;
266+ dictDecompress . set ( null ) ;
235267 }
236268 }
237269
0 commit comments