1+ /*-
2+ * #%L
3+ * Not HDF5
4+ * %%
5+ * Copyright (C) 2017 - 2025 Stephan Saalfeld
6+ * %%
7+ * Redistribution and use in source and binary forms, with or without
8+ * modification, are permitted provided that the following conditions are met:
9+ *
10+ * 1. Redistributions of source code must retain the above copyright notice,
11+ * this list of conditions and the following disclaimer.
12+ * 2. Redistributions in binary form must reproduce the above copyright notice,
13+ * this list of conditions and the following disclaimer in the documentation
14+ * and/or other materials provided with the distribution.
15+ *
16+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+ * POSSIBILITY OF SUCH DAMAGE.
27+ * #L%
28+ */
29+ package org .janelia .saalfeldlab .n5 ;
30+
31+ import java .util .Optional ;
32+ import java .util .concurrent .ConcurrentHashMap ;
33+ import java .util .concurrent .locks .ReentrantReadWriteLock ;
34+ import java .util .concurrent .locks .Lock ;
35+
36+ /**
37+ * A lock manager that provides thread-safe read/write locking for keys.
38+ *
39+ * This class manages a set of {@link ReentrantReadWriteLock}s, one per key,
40+ * allowing multiple threads to read the same key simultaneously while ensuring
41+ * exclusive access for writes.
42+ *
43+ * Unlike file locks which operate at the process level, this provides
44+ * thread-level locking within a single JVM.
45+ */
46+ public class KeyLock {
47+
48+ private final ConcurrentHashMap <String , ReentrantReadWriteLock > locks = new ConcurrentHashMap <>();
49+
50+ /**
51+ * Acquires a read lock for the specified key. Multiple threads can hold
52+ * read locks for the same key simultaneously.
53+ *
54+ * @param key
55+ * the key to lock for reading
56+ * @return a {@link Lock} that must be unlocked when done
57+ */
58+ public Lock lockForReading (String key ) {
59+
60+ ReentrantReadWriteLock rwLock = locks .computeIfAbsent (key , k -> new ReentrantReadWriteLock ());
61+ Lock readLock = rwLock .readLock ();
62+ readLock .lock ();
63+ return readLock ;
64+ }
65+
66+ /**
67+ * Acquires a write lock for the specified key. Only one thread can hold a
68+ * write lock for a key at a time.
69+ *
70+ * @param key
71+ * the key to lock for writing
72+ * @return a {@link Lock} that must be unlocked when done
73+ */
74+ public Lock lockForWriting (String key ) {
75+
76+ ReentrantReadWriteLock rwLock = locks .computeIfAbsent (key , k -> new ReentrantReadWriteLock ());
77+ Lock writeLock = rwLock .writeLock ();
78+ writeLock .lock ();
79+ return writeLock ;
80+ }
81+
82+ /**
83+ * Attempts to acquire a read lock for the specified key without blocking.
84+ *
85+ * @param key
86+ * the key to lock for reading
87+ * @return a {@link Lock} if the lock was acquired, null otherwise
88+ */
89+ public Lock tryLockForReading (String key ) {
90+
91+ ReentrantReadWriteLock rwLock = locks .computeIfAbsent (key , k -> new ReentrantReadWriteLock ());
92+ Lock readLock = rwLock .readLock ();
93+ if (readLock .tryLock ()) {
94+ return readLock ;
95+ }
96+ return null ;
97+ }
98+
99+ /**
100+ * Attempts to acquire a write lock for the specified key without blocking.
101+ *
102+ * @param key
103+ * the key to lock for writing
104+ * @return a {@link Lock} if the lock was acquired, null otherwise
105+ */
106+ public Lock tryLockForWriting (String key ) {
107+
108+ ReentrantReadWriteLock rwLock = locks .computeIfAbsent (key , k -> new ReentrantReadWriteLock ());
109+ Lock writeLock = rwLock .writeLock ();
110+ if (writeLock .tryLock ()) {
111+ return writeLock ;
112+ }
113+ return null ;
114+ }
115+
116+ /**
117+ * Returns the number of keys currently being tracked.
118+ *
119+ * @return the number of keys with associated locks
120+ */
121+ public int size () {
122+
123+ return locks .size ();
124+ }
125+
126+ /**
127+ * Removes the lock for a key if it is not currently held. This can be used
128+ * to clean up unused locks to prevent memory leaks.
129+ *
130+ * @param key
131+ * the key whose lock should be removed
132+ * @return true if the lock was removed, false if it's currently in use
133+ */
134+ public boolean removeLockIfUnused (String key ) {
135+
136+ ReentrantReadWriteLock rwLock = locks .get (key );
137+ if (rwLock != null && !rwLock .isWriteLocked () && rwLock .getReadLockCount () == 0 ) {
138+ return locks .remove (key , rwLock );
139+ }
140+ return false ;
141+ }
142+
143+ public Optional <ReentrantReadWriteLock > getKeyLock (String key ) {
144+ // TODO doc me
145+ return Optional .ofNullable (locks .get (key ));
146+ }
147+
148+ /**
149+ * Clears all unused locks from the lock map. Locks that are currently held
150+ * will not be removed.
151+ *
152+ * @return the number of locks that were removed
153+ */
154+ public int clearUnusedLocks () {
155+
156+ int removed = 0 ;
157+ for (String key : locks .keySet ()) {
158+ if (removeLockIfUnused (key )) {
159+ removed ++;
160+ }
161+ }
162+ return removed ;
163+ }
164+ }
0 commit comments