diff --git a/build.xml b/build.xml index 5ba1b0e143..a528e1a0f4 100644 --- a/build.xml +++ b/build.xml @@ -1232,6 +1232,7 @@ cd .. + diff --git a/src/com/sun/jna/internal/Cleaner.java b/src/com/sun/jna/internal/Cleaner.java index a2095937fc..8213915887 100644 --- a/src/com/sun/jna/internal/Cleaner.java +++ b/src/com/sun/jna/internal/Cleaner.java @@ -27,6 +27,10 @@ import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,154 +39,237 @@ * objects. It replaces the {@code Object#finalize} based resource deallocation * that is deprecated for removal from the JDK. * - *

This class is intented to be used only be JNA itself.

+ *

This class is intended to be used only be JNA itself.

*/ public class Cleaner { - private static final Cleaner INSTANCE = new Cleaner(); + private static final Logger LOG = Logger.getLogger(Cleaner.class.getName()); - public static Cleaner getCleaner() { - return INSTANCE; - } + /* General idea: + * + * There's one Cleaner per thread, kept in a ThreadLocal static variable. + * This instance handles all to-be-cleaned objects registered by this + * thread. Whenever the thread registers another object, it first checks + * if there are references in the queue and cleans them up, then continues + * with the registration. + * + * This leaves two cases open, for which we employ a "Master Cleaner" and + * a separate cleaning thread. + * 1. If a long-lived thread registers some objects in the beginning, but + * then stops registering more objects, the previously registered + * objects will never be cleared. + * 2. When a thread exits before all its registered objects have been + * cleared, the ThreadLocal instance is lost, and so are the pending + * objects. + * + * The Master Cleaner handles the first issue by regularly checking the + * activity of the Cleaners registered with it, and taking over the queues + * of any cleaners appearing to be idle. + * Similarly, the second issue is handled by taking over the queues of threads + * that have terminated. + */ - private final ReferenceQueue referenceQueue; - private Thread cleanerThread; - private CleanerRef firstCleanable; - - private Cleaner() { - referenceQueue = new ReferenceQueue<>(); - } + public static final long MASTER_CLEANUP_INTERVAL_MS = 5000; + public static final long MASTER_MAX_LINGER_MS = 30000; - public synchronized Cleanable register(Object obj, Runnable cleanupTask) { - // The important side effect is the PhantomReference, that is yielded - // after the referent is GCed - return add(new CleanerRef(this, obj, referenceQueue, cleanupTask)); - } + private static class CleanerImpl { + protected final ReferenceQueue referenceQueue = new ReferenceQueue(); + protected final Map cleanables = new ConcurrentHashMap(); + private final AtomicBoolean lock = new AtomicBoolean(false); - private synchronized CleanerRef add(CleanerRef ref) { - synchronized (referenceQueue) { - if (firstCleanable == null) { - firstCleanable = ref; - } else { - ref.setNext(firstCleanable); - firstCleanable.setPrevious(ref); - firstCleanable = ref; - } - if (cleanerThread == null) { - Logger.getLogger(Cleaner.class.getName()).log(Level.FINE, "Starting CleanerThread"); - cleanerThread = new CleanerThread(); - cleanerThread.start(); + private void cleanQueue() { + if (lock.compareAndSet(false, true)) { + try { + Reference ref; + while ((ref = referenceQueue.poll()) != null) { + try { + if (ref instanceof Cleanable) { + ((Cleanable) ref).clean(); + } + } catch (RuntimeException ex) { + Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); + } + } + } finally { + lock.set(false); + } } - return ref; } - } - private synchronized boolean remove(CleanerRef ref) { - synchronized (referenceQueue) { - boolean inChain = false; - if (ref == firstCleanable) { - firstCleanable = ref.getNext(); - inChain = true; - } - if (ref.getPrevious() != null) { - ref.getPrevious().setNext(ref.getNext()); - } - if (ref.getNext() != null) { - ref.getNext().setPrevious(ref.getPrevious()); - } - if (ref.getPrevious() != null || ref.getNext() != null) { - inChain = true; - } - ref.setNext(null); - ref.setPrevious(null); - return inChain; + public Cleanable register(Object obj, Runnable cleanupTask) { + cleanQueue(); + // The important side effect is the PhantomReference, that is yielded + // after the referent is GCed + return new CleanerRef(this, obj, referenceQueue, cleanupTask); } - } - private static class CleanerRef extends PhantomReference implements Cleanable { - private final Cleaner cleaner; - private final Runnable cleanupTask; - private CleanerRef previous; - private CleanerRef next; + protected void put(long n, CleanerRef ref) { + cleanables.put(n, ref); + } - public CleanerRef(Cleaner cleaner, Object referent, ReferenceQueue q, Runnable cleanupTask) { - super(referent, q); - this.cleaner = cleaner; - this.cleanupTask = cleanupTask; + protected boolean remove(long n) { + return cleanables.remove(n) != null; } + } - @Override - public void clean() { - if(cleaner.remove(this)) { - cleanupTask.run(); + static class MasterCleaner extends Cleaner { + static MasterCleaner INSTANCE; + + public static synchronized void add(Cleaner cleaner) { + if (INSTANCE == null) { + INSTANCE = new MasterCleaner(); } + INSTANCE.cleaners.add(cleaner); } - CleanerRef getPrevious() { - return previous; + /** @return true if the caller thread can terminate */ + private static synchronized boolean deleteIfEmpty(MasterCleaner caller) { + if (INSTANCE == caller && INSTANCE.cleaners.isEmpty()) { + INSTANCE = null; + } + return caller.cleaners.isEmpty(); } - void setPrevious(CleanerRef previous) { - this.previous = previous; + /* The lifecycle of a Cleaner instance consists of four phases: + * 1. New instances are contained in Cleaner.INSTANCES and added to a MasterCleaner.cleaners set. + * 2. At some point, the master cleaner takes control of the instance by removing it + * from Cleaner.INSTANCES and MasterCleaner.cleaners, and then adding it to its + * referencedCleaners and watchedCleaners sets. Note that while it is no longer + * in Cleaner.INSTANCES, a thread may still be holding a reference to it. + * 3. Possibly some time later, the last reference to the cleaner instance is dropped and + * it is GC'd. It is then also removed from referencedCleaners but remains in watchedCleaners. + * 4. The master cleaner continues to monitor the watchedCleaners until they are empty and no + * longer referenced. At that point they are also removed from watchedCleaners. + */ + final Set cleaners = Collections.synchronizedSet(new HashSet<>()); + final Set referencedCleaners = new HashSet<>(); + final Set watchedCleaners = new HashSet<>(); + + private MasterCleaner() { + Thread cleanerThread = new Thread(() -> { + long lastNonEmpty = System.currentTimeMillis(); + long now; + long lastMasterRun = 0; + while ((now = System.currentTimeMillis()) < lastNonEmpty + MASTER_MAX_LINGER_MS || !deleteIfEmpty(MasterCleaner.this)) { + if (!cleaners.isEmpty()) { lastNonEmpty = now; } + try { + Reference ref = impl.referenceQueue.remove(MASTER_CLEANUP_INTERVAL_MS); + if(ref instanceof CleanerRef) { + ((CleanerRef) ref).clean(); + } + // "now" is not really *now* at this point, but off by no more than MASTER_CLEANUP_INTERVAL_MS + if (lastMasterRun + MASTER_CLEANUP_INTERVAL_MS <= now) { + masterCleanup(); + lastMasterRun = now; + } + } catch (InterruptedException ex) { + // Can be raised on shutdown. If anyone else messes with + // our reference queue, well, there is no way to separate + // the two cases. + // https://groups.google.com/g/jna-users/c/j0fw96PlOpM/m/vbwNIb2pBQAJ + break; + } catch (Exception ex) { + Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); + } + } + LOG.log(Level.FINE, "MasterCleaner thread {0} exiting", Thread.currentThread()); + }, "JNA Cleaner"); + LOG.log(Level.FINE, "Starting new MasterCleaner thread {0}", cleanerThread); + cleanerThread.setDaemon(true); + cleanerThread.start(); } - CleanerRef getNext() { - return next; + private void masterCleanup() { + for (Iterator> it = Cleaner.INSTANCES.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (!cleaners.contains(entry.getValue())) { continue; } + Cleaner cleaner = entry.getValue(); + long currentCount = cleaner.counter.get(); + if (currentCount == cleaner.lastCount // no new cleanables registered since last master cleanup interval -> assume it is no longer in use + || !entry.getKey().isAlive()) { // owning thread died -> assume it is no longer in use + it.remove(); + CleanerImpl impl = cleaner.impl; + LOG.log(Level.FINE, () -> "MasterCleaner stealing cleaner " + impl + " from thread " + entry.getKey()); + referencedCleaners.add(impl); + watchedCleaners.add(impl); + register(cleaner, () -> { + referencedCleaners.remove(impl); + LOG.log(Level.FINE, "Cleaner {0} no longer referenced", impl); + }); + cleaners.remove(cleaner); + } else { + cleaner.lastCount = currentCount; + } + } + + for (Iterator it = watchedCleaners.iterator(); it.hasNext(); ) { + CleanerImpl impl = it.next(); + impl.cleanQueue(); + if (!referencedCleaners.contains(impl)) { + if (impl.cleanables.isEmpty()) { + it.remove(); + LOG.log(Level.FINE, "Discarding empty Cleaner {0}", impl); + } + } + } } + } + + private static final Map INSTANCES = new ConcurrentHashMap<>(); + + public static Cleaner getCleaner() { + return INSTANCES.computeIfAbsent(Thread.currentThread(), Cleaner::new); + } + + protected final CleanerImpl impl; + protected final Thread owner; + protected final AtomicLong counter = new AtomicLong(Long.MIN_VALUE); + protected long lastCount; // used by MasterCleaner only - void setNext(CleanerRef next) { - this.next = next; + private Cleaner() { + this(null); + } + + private Cleaner(Thread owner) { + impl = new CleanerImpl(); + this.owner = owner; + if (owner != null) { + MasterCleaner.add(this); } + LOG.log(Level.FINE, () -> owner == null ? "Created new MasterCleaner" + : "Created new Cleaner " + impl + " for thread " + owner); } - public static interface Cleanable { - public void clean(); + public Cleanable register(Object obj, Runnable cleanupTask) { + counter.incrementAndGet(); + return impl.register(obj, cleanupTask); } - private class CleanerThread extends Thread { + private static class CleanerRef extends PhantomReference implements Cleanable { + private static final AtomicLong COUNTER = new AtomicLong(Long.MIN_VALUE); - private static final long CLEANER_LINGER_TIME = 30000; + private final CleanerImpl cleaner; + private final long number = COUNTER.incrementAndGet(); + private Runnable cleanupTask; - public CleanerThread() { - super("JNA Cleaner"); - setDaemon(true); + public CleanerRef(CleanerImpl impl, Object referent, ReferenceQueue q, Runnable cleanupTask) { + super(referent, q); + LOG.log(Level.FINER, () -> "Registering " + referent + " with " + impl + " as " + this); + this.cleaner = impl; + this.cleanupTask = cleanupTask; + cleaner.put(number, this); } @Override - public void run() { - while (true) { - try { - Reference ref = referenceQueue.remove(CLEANER_LINGER_TIME); - if (ref instanceof CleanerRef) { - ((CleanerRef) ref).clean(); - } else if (ref == null) { - synchronized (referenceQueue) { - Logger logger = Logger.getLogger(Cleaner.class.getName()); - if (firstCleanable == null) { - cleanerThread = null; - logger.log(Level.FINE, "Shutting down CleanerThread"); - break; - } else if (logger.isLoggable(Level.FINER)) { - StringBuilder registeredCleaners = new StringBuilder(); - for(CleanerRef cleanerRef = firstCleanable; cleanerRef != null; cleanerRef = cleanerRef.next) { - if(registeredCleaners.length() != 0) { - registeredCleaners.append(", "); - } - registeredCleaners.append(cleanerRef.cleanupTask.toString()); - } - logger.log(Level.FINER, "Registered Cleaners: {0}", registeredCleaners.toString()); - } - } - } - } catch (InterruptedException ex) { - // Can be raised on shutdown. If anyone else messes with - // our reference queue, well, there is no way to separate - // the two cases. - // https://groups.google.com/g/jna-users/c/j0fw96PlOpM/m/vbwNIb2pBQAJ - break; - } catch (Exception ex) { - Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex); - } + public void clean() { + if(cleaner.remove(this.number) && cleanupTask != null) { + LOG.log(Level.FINER, "Cleaning up {0}", this); + cleanupTask.run(); + cleanupTask = null; } } } + + public interface Cleanable { + void clean(); + } } diff --git a/test/com/sun/jna/CallbacksTest.java b/test/com/sun/jna/CallbacksTest.java index ff801fb7e5..4780615cb4 100644 --- a/test/com/sun/jna/CallbacksTest.java +++ b/test/com/sun/jna/CallbacksTest.java @@ -34,10 +34,10 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; import com.sun.jna.Callback.UncaughtExceptionHandler; import com.sun.jna.CallbacksTest.TestLibrary.CbCallback; +import com.sun.jna.internal.Cleaner; import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.PointerByReference; import com.sun.jna.win32.W32APIOptions; @@ -79,6 +79,24 @@ protected void waitFor(Thread thread) { } } + public static abstract class Condition { + protected final T obj; + + public Condition(T t) { obj = t; } + + public boolean evaluate() { return evaluate(obj); } + + abstract boolean evaluate(T t); + } + public static void waitForGc(Condition condition) throws Exception { + for (int i = 0; i < 2 * Cleaner.MASTER_CLEANUP_INTERVAL_MS / 10 + 5 && condition.evaluate(); ++i) { + synchronized (CallbacksTest.class) { // cleanups happen in a different thread, make sure we see it here + System.gc(); + } + Thread.sleep(10); // Give the GC a chance to run + } + } + public static class SmallTestStructure extends Structure { public static final List FIELDS = createFieldsOrder("value"); public double value; @@ -354,31 +372,26 @@ public void callback() { lib.callVoidCallback(cb); assertTrue("Callback not called", called[0]); - Map refs = new WeakHashMap<>(callbackCache()); - assertTrue("Callback not cached", refs.containsKey(cb)); + assertTrue("Callback not cached", callbackCache().containsKey(cb)); + final Map refs = callbackCache(); CallbackReference ref = refs.get(cb); - refs = callbackCache(); - Pointer cbstruct = ref.cbstruct; + final Pointer cbstruct = ref.cbstruct; cb = null; - System.gc(); - for (int i = 0; i < 100 && (ref.get() != null || refs.containsValue(ref)); ++i) { - Thread.sleep(10); // Give the GC a chance to run - System.gc(); - } + waitForGc(new Condition(ref) { + public boolean evaluate(CallbackReference _ref) { + return _ref.get() != null || refs.containsValue(_ref); + } + }); assertNull("Callback not GC'd", ref.get()); assertFalse("Callback still in map", refs.containsValue(ref)); ref = null; - System.gc(); - for (int i = 0; i < 100 && (cbstruct.peer != 0 || refs.size() > 0); ++i) { - // Flush weak hash map - refs.size(); - try { - Thread.sleep(10); // Give the GC a chance to run - System.gc(); - } finally {} - } + waitForGc(new Condition(null) { + public boolean evaluate(Object o) { + return refs.size() > 0 || cbstruct.peer != 0; + } + }); assertEquals("Callback trampoline not freed", 0, cbstruct.peer); } @@ -678,7 +691,7 @@ public String callback(String arg, String arg2) { assertEquals("Wrong String return", VALUE + VALUE2, value); } - public void testStringCallbackMemoryReclamation() throws InterruptedException { + public void testStringCallbackMemoryReclamation() throws Exception { TestLibrary.StringCallback cb = new TestLibrary.StringCallback() { @Override public String callback(String arg, String arg2) { @@ -687,24 +700,22 @@ public String callback(String arg, String arg2) { }; // A little internal groping - Map m = CallbackReference.allocations; + final Map m = CallbackReference.allocations; m.clear(); Charset charset = Charset.forName(Native.getDefaultStringEncoding()); String arg = getName() + "1" + charset.decode(charset.encode(UNICODE)); String arg2 = getName() + "2" + charset.decode(charset.encode(UNICODE)); String value = lib.callStringCallback(cb, arg, arg2); - WeakReference ref = new WeakReference<>(value); + final WeakReference ref = new WeakReference<>(value); arg = null; value = null; - System.gc(); - for (int i = 0; i < 100 && (ref.get() != null || m.size() > 0); ++i) { - try { - Thread.sleep(10); // Give the GC a chance to run - System.gc(); - } finally {} - } + waitForGc(new Condition(null) { + public boolean evaluate(Object o) { + return m.size() > 0 || ref.get() != null; + } + }); assertNull("NativeString reference not GC'd", ref.get()); assertEquals("NativeString reference still held: " + m.values(), 0, m.size()); } @@ -1477,30 +1488,27 @@ public void callback() { assertEquals("Wrong module HANDLE for DLL function pointer", handle, pref.getValue()); // Check slot re-use - Map refs = new WeakHashMap<>(callbackCache()); - assertTrue("Callback not cached", refs.containsKey(cb)); + assertTrue("Callback not cached", callbackCache().containsKey(cb)); + final Map refs = callbackCache(); CallbackReference ref = refs.get(cb); - refs = callbackCache(); Pointer cbstruct = ref.cbstruct; Pointer first_fptr = cbstruct.getPointer(0); cb = null; - System.gc(); - for (int i = 0; i < 100 && (ref.get() != null || refs.containsValue(ref)); ++i) { - Thread.sleep(10); // Give the GC a chance to run - System.gc(); - } + waitForGc(new Condition(ref) { + public boolean evaluate(CallbackReference _ref) { + return _ref.get() != null || refs.containsValue(_ref); + } + }); assertNull("Callback not GC'd", ref.get()); assertFalse("Callback still in map", refs.containsValue(ref)); ref = null; - System.gc(); - for (int i = 0; i < 100 && (cbstruct.peer != 0 || refs.size() > 0); ++i) { - // Flush weak hash map - refs.size(); - Thread.sleep(10); // Give the GC a chance to run - System.gc(); - } + waitForGc(new Condition(cbstruct) { + public boolean evaluate(Pointer p) { + return refs.size() > 0 || p.peer != 0; + } + }); assertEquals("Callback trampoline not freed", 0, cbstruct.peer); // Next allocation should be at same place diff --git a/test/com/sun/jna/MasterCleanerTest.java b/test/com/sun/jna/MasterCleanerTest.java new file mode 100644 index 0000000000..a64b5183ea --- /dev/null +++ b/test/com/sun/jna/MasterCleanerTest.java @@ -0,0 +1,115 @@ +/* Copyright (c) 2024 Peter Conrad, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna; + +import com.sun.jna.internal.MasterAccessor; +import junit.framework.TestCase; + +import java.util.function.Supplier; + +public class MasterCleanerTest extends TestCase { + private CallbacksTest.TestLibrary lib; + + public static boolean waitFor(Supplier cond, long maxWaitMs) { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() <= start + maxWaitMs && !cond.get()) { + try { + Thread.sleep(10); + } catch (InterruptedException ignore) {} + } + return cond.get(); + } + + @Override + protected void setUp() { + lib = Native.load("testlib", CallbacksTest.TestLibrary.class); + } + + @Override + protected void tearDown() { + lib = null; + } + + public void testGCCallbackOnFinalize() throws Exception { + final boolean[] latch = { false }; + Thread thread = new Thread(() -> { + CallbacksTest.TestLibrary.VoidCallback cb = new CallbacksTest.TestLibrary.VoidCallback() { + @Override + public void callback() { + latch[0] = true; + } + }; + synchronized (latch) { + lib.callVoidCallback(cb); + latch.notifyAll(); + try { + latch.wait(); + } catch (InterruptedException ignore) {} + } + }); + thread.start(); + + CallbackReference ref; + synchronized (latch) { + if (!latch[0]) { + latch.wait(); + } + assertTrue(latch[0]); + // Assert thread is running and has registered a callback and master is running too + assertTrue(thread.isAlive()); + assertTrue(MasterAccessor.masterIsRunning()); + assertFalse(MasterAccessor.getCleanerImpls().isEmpty()); + assertEquals(1, CallbackReference.callbackMap.size()); + ref = CallbackReference.callbackMap.values().iterator().next(); + CallbacksTest.waitForGc(new CallbacksTest.Condition(ref) { + public boolean evaluate(CallbackReference _ref) { + return _ref.get() != null || CallbackReference.callbackMap.containsValue(_ref); + } + }); + assertNotNull("Callback GC'd prematurely", ref.get()); + assertTrue("Callback no longer in map", CallbackReference.callbackMap.containsValue(ref)); + latch.notifyAll(); + } + thread.join(); + + // thread is no longer running, dummy is collectable, master is still running + final Pointer cbstruct = ref.cbstruct; + assertTrue(MasterAccessor.masterIsRunning()); + CallbacksTest.waitForGc(new CallbacksTest.Condition(ref) { + public boolean evaluate(CallbackReference _ref) { + return _ref.get() != null || CallbackReference.callbackMap.containsValue(_ref); + } + }); + assertNull("Callback not GC'd", ref.get()); + assertFalse("Callback still in map", CallbackReference.callbackMap.containsValue(ref)); + + assertTrue("CleanerImpl still exists", + waitFor(() -> MasterAccessor.getCleanerImpls().isEmpty(), 60_000)); // 1 minute + + thread = null; + // thread is collectable -> wait until master terminates + assertTrue("Master still running", + waitFor(() -> !MasterAccessor.masterIsRunning(), 60_000)); // 1 minute + } +} diff --git a/test/com/sun/jna/different_package/CleanerTest.java b/test/com/sun/jna/different_package/CleanerTest.java new file mode 100644 index 0000000000..aabc22bcb8 --- /dev/null +++ b/test/com/sun/jna/different_package/CleanerTest.java @@ -0,0 +1,70 @@ +/* Copyright (c) 2023 Peter Conrad, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.different_package; + +import com.sun.jna.Structure; +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +public class CleanerTest extends TestCase { + private static final int NUM_THREADS = 16; + private static final long ITERATIONS = 100000; + + @Structure.FieldOrder({"bytes"}) + public static class Dummy extends Structure { + public byte[] bytes; + + public Dummy() {} + + public Dummy(byte[] what) { bytes = what; } + } + + private static class Allocator implements Runnable { + @Override + public void run() { + for (long i = 0; i < ITERATIONS; ++i) { + Dummy d = new Dummy(new byte[1024]); + d.write(); + } + } + } + + public void testOOM() { + List threads = new ArrayList<>(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; ++i) { + Thread t = new Thread(new Allocator()); + t.start(); + threads.add(t); + } + for (Thread t : threads) { + while (t.isAlive()) { + try { + t.join(); + } catch (InterruptedException ignore) {} + } + } + } +} diff --git a/test/com/sun/jna/internal/MasterAccessor.java b/test/com/sun/jna/internal/MasterAccessor.java new file mode 100644 index 0000000000..331a72157d --- /dev/null +++ b/test/com/sun/jna/internal/MasterAccessor.java @@ -0,0 +1,37 @@ +/* Copyright (c) 2024 Peter Conrad, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.internal; + +import java.util.Set; + +public class MasterAccessor { + public static synchronized boolean masterIsRunning() { // synchronized is for memory synch not mutex + return Cleaner.MasterCleaner.INSTANCE != null; + } + + public static synchronized Set getCleanerImpls() { // synchronized is for memory synch not mutex + Cleaner.MasterCleaner mc = Cleaner.MasterCleaner.INSTANCE; + return mc == null ? null : mc.cleaners; + } +}