ChangeListenerCleaner.java
package sprouts.impl;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.List;
/**
* This class stores actions which are being executed when an associated object is being garbage collected.
* This class is similar to the cleaner class introduced in JDK 11, however the minimal version compatibility target
* for Sprouts is Java 8, which means that this cleaner class introduced in Java 11 is not available here!
* That is why a custom cleaner implementation is needed.<br>
* <br> <br>
* <b>Warning: This is an internal class, meaning it should not be used
* anywhere but within this library. <br>
* This class or its public methods might change or get removed in future versions!</b>
*/
final class ChangeListenerCleaner
{
private static final Logger log = LoggerFactory.getLogger(ChangeListenerCleaner.class);
private static final ChangeListenerCleaner _INSTANCE = new ChangeListenerCleaner();
private static final long _QUEUE_TIMEOUT = 60 * 1000;
public static ChangeListenerCleaner getInstance() {
return _INSTANCE;
}
private final ReferenceQueue<Object> _referenceQueue = new ReferenceQueue<>();
private final List<ReferenceWithCleanup<Object>> _toBeCleaned = new ArrayList<>();
private final Thread _thread;
private ChangeListenerCleaner() {
_thread = new Thread(this::run, "Sprouts-Cleaner");
}
static class ReferenceWithCleanup<T> extends PhantomReference<T>
{
private @Nullable Runnable _action;
ReferenceWithCleanup( T o, Runnable action, ReferenceQueue<T> queue ) {
super( o, queue );
_action = action;
}
public void cleanup() {
if ( _action != null ) {
try {
_action.run();
} catch (Exception e) {
log.error("Failed to execute cleanup action '"+_action+"'.", e);
} finally {
_action = null;
}
}
}
}
public void register( @Nullable Object o, Runnable action ) {
if ( o == null ) {
log.warn("Attempt to register a null object for cleanup. This is not allowed!");
try {
action.run();
} catch (Exception e) {
log.error("Failed to execute cleanup action '"+action+"'.", e);
}
return;
}
synchronized ( _referenceQueue ) {
_toBeCleaned.add(new ReferenceWithCleanup<>(o, action, _referenceQueue));
if ( _toBeCleaned.size() == 1 ) {
if ( !_thread.isAlive() ) {
_thread.start();
}
else {
// We notify the cleaner thread that there are new items to be cleaned
synchronized ( _thread ) {
_thread.notify();
}
}
}
}
}
private void run() {
if ( !_thread.isAlive() ) {
_thread.start();
}
while ( _thread.isAlive() ) {
while ( !_toBeCleaned.isEmpty() ) {
checkCleanup();
}
try {
synchronized ( _thread ) {
_thread.wait();
}
} catch (Exception e) {
log.error("Failed to make cleaner thread wait for cleaning notification!", e);
}
}
}
private void checkCleanup() {
try {
ReferenceWithCleanup<Object> ref = (ReferenceWithCleanup<Object>) _referenceQueue.remove(_QUEUE_TIMEOUT);
if ( ref != null ) {
try {
ref.cleanup();
} catch ( Throwable e ) {
log.error("Failed to perform cleanup!", e);
} finally {
_toBeCleaned.remove(ref);
}
}
} catch ( Throwable e ) {
log.error("Failed to call 'remove()' on cleaner internal queue.", e);
}
}
@Override
public String toString() {
return this.getClass().getSimpleName()+"@"+Integer.toHexString(this.hashCode())+"[" +
"registered=" + _toBeCleaned.size() +
"]";
}
}