|
27 | 27 | import java.lang.ref.PhantomReference;
|
28 | 28 | import java.lang.ref.Reference;
|
29 | 29 | import java.lang.ref.ReferenceQueue;
|
| 30 | +import java.util.Iterator; |
| 31 | +import java.util.Map; |
| 32 | +import java.util.concurrent.ConcurrentHashMap; |
| 33 | +import java.util.concurrent.atomic.AtomicBoolean; |
| 34 | +import java.util.concurrent.atomic.AtomicLong; |
30 | 35 | import java.util.logging.Level;
|
31 | 36 | import java.util.logging.Logger;
|
32 | 37 |
|
|
35 | 40 | * objects. It replaces the {@code Object#finalize} based resource deallocation
|
36 | 41 | * that is deprecated for removal from the JDK.
|
37 | 42 | *
|
38 |
| - * <p><strong>This class is intented to be used only be JNA itself.</strong></p> |
| 43 | + * <p><strong>This class is intended to be used only be JNA itself.</strong></p> |
39 | 44 | */
|
40 | 45 | public class Cleaner {
|
41 |
| - private static final Cleaner INSTANCE = new Cleaner(); |
| 46 | + /* General idea: |
| 47 | + * |
| 48 | + * There's one Cleaner per thread, kept in a ThreadLocal static variable. |
| 49 | + * This instance handles all to-be-cleaned objects registered by this |
| 50 | + * thread. Whenever the thread registers another object, it first checks |
| 51 | + * if there are references in the queue and cleans them up, then continues |
| 52 | + * with the registration. |
| 53 | + * |
| 54 | + * This leaves two cases open, for which we employ a "Master Cleaner" and |
| 55 | + * a separate cleaning thread. |
| 56 | + * 1. If a long-lived thread registers some objects in the beginning, but |
| 57 | + * then stops registering more objects, the previously registered |
| 58 | + * objects will never be cleared. |
| 59 | + * 2. When a thread exists before all its registered objects have been |
| 60 | + * cleared, the ThreadLocal instance are lost, and so are the pending |
| 61 | + * objects. |
| 62 | + * |
| 63 | + * The Master Cleaner handles the first issue by regularly handling the |
| 64 | + * queues of the Cleaners registered with it. |
| 65 | + * The seconds issue is handled by registering the per-thread Cleaner |
| 66 | + * instances with the Master's reference queue. |
| 67 | + */ |
| 68 | + |
| 69 | + private static class CleanerImpl { |
| 70 | + protected final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>(); |
| 71 | + protected final Map<Long,CleanerRef> cleanables = new ConcurrentHashMap<Long,CleanerRef>(); |
| 72 | + private final AtomicBoolean lock = new AtomicBoolean(false); |
| 73 | + |
| 74 | + private void cleanQueue() { |
| 75 | + if (lock.compareAndSet(false, true)) { |
| 76 | + try { |
| 77 | + Reference<?> ref; |
| 78 | + while ((ref = referenceQueue.poll()) != null) { |
| 79 | + try { |
| 80 | + if (ref instanceof Cleanable) { |
| 81 | + ((Cleanable) ref).clean(); |
| 82 | + } |
| 83 | + } catch (RuntimeException ex) { |
| 84 | + Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); |
| 85 | + } |
| 86 | + } |
| 87 | + } finally { |
| 88 | + lock.set(false); |
| 89 | + } |
| 90 | + } |
| 91 | + } |
42 | 92 |
|
43 |
| - public static Cleaner getCleaner() { |
44 |
| - return INSTANCE; |
| 93 | + public Cleanable register(Object obj, Runnable cleanupTask) { |
| 94 | + cleanQueue(); |
| 95 | + // The important side effect is the PhantomReference, that is yielded |
| 96 | + // after the referent is GCed |
| 97 | + return new CleanerRef(this, obj, referenceQueue, cleanupTask); |
| 98 | + } |
| 99 | + |
| 100 | + protected void put(long n, CleanerRef ref) { |
| 101 | + cleanables.put(n, ref); |
| 102 | + } |
| 103 | + |
| 104 | + protected boolean remove(long n) { |
| 105 | + return cleanables.remove(n) != null; |
| 106 | + } |
45 | 107 | }
|
46 | 108 |
|
47 |
| - private final ReferenceQueue<Object> referenceQueue; |
48 |
| - private final Thread cleanerThread; |
49 |
| - private CleanerRef firstCleanable; |
50 |
| - |
51 |
| - private Cleaner() { |
52 |
| - referenceQueue = new ReferenceQueue<Object>(); |
53 |
| - cleanerThread = new Thread() { |
54 |
| - @Override |
55 |
| - public void run() { |
56 |
| - while(true) { |
57 |
| - try { |
58 |
| - Reference<? extends Object> ref = referenceQueue.remove(); |
59 |
| - if(ref instanceof CleanerRef) { |
60 |
| - ((CleanerRef) ref).clean(); |
| 109 | + private static class MasterCleanerImpl extends CleanerImpl { |
| 110 | + @Override |
| 111 | + protected synchronized void put(long n, CleanerRef ref) { |
| 112 | + super.put(n, ref); |
| 113 | + } |
| 114 | + |
| 115 | + @Override |
| 116 | + protected synchronized boolean remove(long n) { |
| 117 | + return super.remove(n); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + public static class MasterCleaner extends Cleaner { |
| 122 | + public static final long CLEANUP_INTERVAL_MS = 5000; |
| 123 | + public static final long MAX_LINGER_MS = 30000; |
| 124 | + |
| 125 | + private static MasterCleaner INSTANCE; |
| 126 | + |
| 127 | + public static synchronized void add(Cleaner cleaner) { |
| 128 | + if (INSTANCE == null) { |
| 129 | + INSTANCE = new MasterCleaner(); |
| 130 | + } |
| 131 | + final CleanerImpl impl = cleaner.impl; |
| 132 | + INSTANCE.cleanerImpls.put(impl, true); |
| 133 | + INSTANCE.register(cleaner, new Runnable() { |
| 134 | + @Override |
| 135 | + public void run() { |
| 136 | + INSTANCE.cleanerImpls.put(impl, false); |
| 137 | + } |
| 138 | + }); |
| 139 | + } |
| 140 | + |
| 141 | + private static synchronized boolean deleteIfEmpty() { |
| 142 | + if (INSTANCE != null && INSTANCE.cleanerImpls.isEmpty()) { |
| 143 | + INSTANCE = null; |
| 144 | + return true; |
| 145 | + } |
| 146 | + return false; |
| 147 | + } |
| 148 | + |
| 149 | + private final Map<CleanerImpl,Boolean> cleanerImpls = new ConcurrentHashMap<CleanerImpl,Boolean>(); |
| 150 | + private long lastNonEmpty = System.currentTimeMillis(); |
| 151 | + |
| 152 | + private MasterCleaner() { |
| 153 | + super(true); |
| 154 | + Thread cleanerThread = new Thread() { |
| 155 | + @Override |
| 156 | + public void run() { |
| 157 | + long now; |
| 158 | + while ((now = System.currentTimeMillis()) < lastNonEmpty + MAX_LINGER_MS || !deleteIfEmpty()) { |
| 159 | + if (!cleanerImpls.isEmpty()) { lastNonEmpty = now; } |
| 160 | + try { |
| 161 | + Reference<?> ref = impl.referenceQueue.remove(CLEANUP_INTERVAL_MS); |
| 162 | + if(ref instanceof CleanerRef) { |
| 163 | + ((CleanerRef) ref).clean(); |
| 164 | + } else { |
| 165 | + masterCleanup(); |
| 166 | + } |
| 167 | + } catch (InterruptedException ex) { |
| 168 | + // Can be raised on shutdown. If anyone else messes with |
| 169 | + // our reference queue, well, there is no way to separate |
| 170 | + // the two cases. |
| 171 | + // https://groups.google.com/g/jna-users/c/j0fw96PlOpM/m/vbwNIb2pBQAJ |
| 172 | + break; |
| 173 | + } catch (Exception ex) { |
| 174 | + Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); |
61 | 175 | }
|
62 |
| - } catch (InterruptedException ex) { |
63 |
| - // Can be raised on shutdown. If anyone else messes with |
64 |
| - // our reference queue, well, there is no way to separate |
65 |
| - // the two cases. |
66 |
| - // https://groups.google.com/g/jna-users/c/j0fw96PlOpM/m/vbwNIb2pBQAJ |
67 |
| - break; |
68 |
| - } catch (Exception ex) { |
69 |
| - Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); |
70 | 176 | }
|
71 | 177 | }
|
| 178 | + }; |
| 179 | + cleanerThread.setName("JNA Cleaner"); |
| 180 | + cleanerThread.setDaemon(true); |
| 181 | + cleanerThread.start(); |
| 182 | + } |
| 183 | + |
| 184 | + private void masterCleanup() { |
| 185 | + Iterator<Map.Entry<CleanerImpl,Boolean>> it = cleanerImpls.entrySet().iterator(); |
| 186 | + while (it.hasNext()) { |
| 187 | + Map.Entry<CleanerImpl,Boolean> entry = it.next(); |
| 188 | + entry.getKey().cleanQueue(); |
| 189 | + if (!entry.getValue() && entry.getKey().cleanables.isEmpty()) { |
| 190 | + it.remove(); |
| 191 | + } |
72 | 192 | }
|
73 |
| - }; |
74 |
| - cleanerThread.setName("JNA Cleaner"); |
75 |
| - cleanerThread.setDaemon(true); |
76 |
| - cleanerThread.start(); |
| 193 | + } |
77 | 194 | }
|
78 | 195 |
|
79 |
| - public synchronized Cleanable register(Object obj, Runnable cleanupTask) { |
80 |
| - // The important side effect is the PhantomReference, that is yielded |
81 |
| - // after the referent is GCed |
82 |
| - return add(new CleanerRef(this, obj, referenceQueue, cleanupTask)); |
| 196 | + private static final ThreadLocal<Cleaner> MY_INSTANCE = new ThreadLocal<Cleaner>() { |
| 197 | + @Override |
| 198 | + protected Cleaner initialValue() { |
| 199 | + return new Cleaner(false); |
| 200 | + } |
| 201 | + }; |
| 202 | + |
| 203 | + public static Cleaner getCleaner() { |
| 204 | + return MY_INSTANCE.get(); |
83 | 205 | }
|
84 | 206 |
|
85 |
| - private synchronized CleanerRef add(CleanerRef ref) { |
86 |
| - if(firstCleanable == null) { |
87 |
| - firstCleanable = ref; |
| 207 | + protected final CleanerImpl impl; |
| 208 | + |
| 209 | + private Cleaner(boolean master) { |
| 210 | + if (master) { |
| 211 | + impl = new MasterCleanerImpl(); |
88 | 212 | } else {
|
89 |
| - ref.setNext(firstCleanable); |
90 |
| - firstCleanable.setPrevious(ref); |
91 |
| - firstCleanable = ref; |
| 213 | + impl = new CleanerImpl(); |
| 214 | + MasterCleaner.add(this); |
92 | 215 | }
|
93 |
| - return ref; |
94 | 216 | }
|
95 | 217 |
|
96 |
| - private synchronized boolean remove(CleanerRef ref) { |
97 |
| - boolean inChain = false; |
98 |
| - if(ref == firstCleanable) { |
99 |
| - firstCleanable = ref.getNext(); |
100 |
| - inChain = true; |
101 |
| - } |
102 |
| - if(ref.getPrevious() != null) { |
103 |
| - ref.getPrevious().setNext(ref.getNext()); |
104 |
| - } |
105 |
| - if(ref.getNext() != null) { |
106 |
| - ref.getNext().setPrevious(ref.getPrevious()); |
107 |
| - } |
108 |
| - if(ref.getPrevious() != null || ref.getNext() != null) { |
109 |
| - inChain = true; |
110 |
| - } |
111 |
| - ref.setNext(null); |
112 |
| - ref.setPrevious(null); |
113 |
| - return inChain; |
| 218 | + public Cleanable register(Object obj, Runnable cleanupTask) { |
| 219 | + return impl.register(obj, cleanupTask); |
114 | 220 | }
|
115 | 221 |
|
116 | 222 | private static class CleanerRef extends PhantomReference<Object> implements Cleanable {
|
117 |
| - private final Cleaner cleaner; |
118 |
| - private final Runnable cleanupTask; |
119 |
| - private CleanerRef previous; |
120 |
| - private CleanerRef next; |
| 223 | + private static final AtomicLong COUNTER = new AtomicLong(Long.MIN_VALUE); |
| 224 | + |
| 225 | + private final CleanerImpl cleaner; |
| 226 | + private final long number = COUNTER.incrementAndGet(); |
| 227 | + private Runnable cleanupTask; |
121 | 228 |
|
122 |
| - public CleanerRef(Cleaner cleaner, Object referent, ReferenceQueue<? super Object> q, Runnable cleanupTask) { |
| 229 | + public CleanerRef(CleanerImpl impl, Object referent, ReferenceQueue<Object> q, Runnable cleanupTask) { |
123 | 230 | super(referent, q);
|
124 |
| - this.cleaner = cleaner; |
| 231 | + this.cleaner = impl; |
125 | 232 | this.cleanupTask = cleanupTask;
|
| 233 | + cleaner.put(number, this); |
126 | 234 | }
|
127 | 235 |
|
128 | 236 | public void clean() {
|
129 |
| - if(cleaner.remove(this)) { |
| 237 | + if(cleaner.remove(this.number) && cleanupTask != null) { |
130 | 238 | cleanupTask.run();
|
| 239 | + cleanupTask = null; |
131 | 240 | }
|
132 | 241 | }
|
133 |
| - |
134 |
| - CleanerRef getPrevious() { |
135 |
| - return previous; |
136 |
| - } |
137 |
| - |
138 |
| - void setPrevious(CleanerRef previous) { |
139 |
| - this.previous = previous; |
140 |
| - } |
141 |
| - |
142 |
| - CleanerRef getNext() { |
143 |
| - return next; |
144 |
| - } |
145 |
| - |
146 |
| - void setNext(CleanerRef next) { |
147 |
| - this.next = next; |
148 |
| - } |
149 | 242 | }
|
150 | 243 |
|
151 | 244 | public static interface Cleanable {
|
|
0 commit comments