-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve performance for Cleaner implementation #1617
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,12 @@ | |
import java.lang.ref.PhantomReference; | ||
import java.lang.ref.Reference; | ||
import java.lang.ref.ReferenceQueue; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.concurrent.locks.ReadWriteLock; | ||
import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
|
||
|
@@ -44,65 +50,79 @@ public static Cleaner getCleaner() { | |
return INSTANCE; | ||
} | ||
|
||
// Guard for trackedObjects and cleanerThread. The readlock is utilized when | ||
// the trackedObjects are manipulated, the writelock protectes starting and | ||
// stopping the CleanerThread | ||
private final ReadWriteLock cleanerThreadLock = new ReentrantReadWriteLock(); | ||
private final ReferenceQueue<Object> referenceQueue; | ||
private Thread cleanerThread; | ||
private CleanerRef firstCleanable; | ||
// Count of objects tracked by the cleaner. When >0 it means objects are | ||
// being tracked by the cleaner and the cleanerThread must be running | ||
private final AtomicLong trackedObjects = new AtomicLong(); | ||
// Map only serves as holder, so that the CleanerRefs stay hard referenced | ||
// and quickly accessible for removal | ||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") | ||
private final Map<CleanerRef,CleanerRef> map = new ConcurrentHashMap<>(); | ||
// Thread to handle the actual cleaning | ||
private volatile Thread cleanerThread; | ||
|
||
private Cleaner() { | ||
referenceQueue = new ReferenceQueue<>(); | ||
} | ||
|
||
public synchronized Cleanable register(Object obj, Runnable cleanupTask) { | ||
@SuppressWarnings("EmptySynchronizedStatement") | ||
public 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)); | ||
try { | ||
return add(new CleanerRef(this, obj, referenceQueue, cleanupTask)); | ||
} finally { | ||
synchronized (obj) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is potentially dangerous. Some other thread could be holding a lock on obj already. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please elaborate, why you think we can deadlock here? For a deadlock we need two different locks, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think of two threads. One locks object A and registers object B, the other one locks B and registers A. |
||
// Used as a reachability fence for obj | ||
// Ensure, that add completes before obj can be collected and | ||
// the cleaner is run | ||
} | ||
} | ||
} | ||
|
||
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 CleanerRef add(CleanerRef ref) { | ||
map.put(ref, ref); | ||
cleanerThreadLock.readLock().lock(); | ||
try { | ||
long count = trackedObjects.incrementAndGet(); | ||
if (cleanerThread == null && count > 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
cleanerThreadLock.readLock().unlock(); | ||
cleanerThreadLock.writeLock().lock(); | ||
try { | ||
if (cleanerThread == null && trackedObjects.get() > 0) { | ||
Logger.getLogger(Cleaner.class.getName()).log(Level.FINE, "Starting CleanerThread"); | ||
cleanerThread = new CleanerThread(); | ||
cleanerThread.start(); | ||
} | ||
} finally { | ||
cleanerThreadLock.readLock().lock(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you are mistaken. The readLock is locked and the writeLock is unlocked. This downgrades the ReadWriteLock from write to read. This is needed, as the readLock is unlocked in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What I meant is that you could set a flag instead and skip the unlock in |
||
cleanerThreadLock.writeLock().unlock(); | ||
} | ||
} | ||
return ref; | ||
} finally { | ||
cleanerThreadLock.readLock().unlock(); | ||
} | ||
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; | ||
private void remove(CleanerRef ref) { | ||
map.remove(ref); | ||
cleanerThreadLock.readLock().lock(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the lock here is not required. Like you said, the lock protects the interaction of |
||
try { | ||
trackedObjects.decrementAndGet(); | ||
} finally { | ||
cleanerThreadLock.readLock().unlock(); | ||
} | ||
} | ||
|
||
private static class CleanerRef extends PhantomReference<Object> implements Cleanable { | ||
private final Cleaner cleaner; | ||
private final Runnable cleanupTask; | ||
private CleanerRef previous; | ||
private CleanerRef next; | ||
private final AtomicBoolean cleaned = new AtomicBoolean(false); | ||
|
||
public CleanerRef(Cleaner cleaner, Object referent, ReferenceQueue<? super Object> q, Runnable cleanupTask) { | ||
super(referent, q); | ||
|
@@ -112,26 +132,11 @@ public CleanerRef(Cleaner cleaner, Object referent, ReferenceQueue<? super Objec | |
|
||
@Override | ||
public void clean() { | ||
if(cleaner.remove(this)) { | ||
if (!cleaned.getAndSet(true)) { | ||
cleaner.remove(this); | ||
cleanupTask.run(); | ||
} | ||
} | ||
|
||
CleanerRef getPrevious() { | ||
return previous; | ||
} | ||
|
||
void setPrevious(CleanerRef previous) { | ||
this.previous = previous; | ||
} | ||
|
||
CleanerRef getNext() { | ||
return next; | ||
} | ||
|
||
void setNext(CleanerRef next) { | ||
this.next = next; | ||
} | ||
} | ||
|
||
public static interface Cleanable { | ||
|
@@ -155,22 +160,28 @@ public void run() { | |
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(", "); | ||
cleanerThreadLock.readLock().lock(); | ||
try { | ||
if (trackedObjects.get() == 0) { | ||
cleanerThreadLock.readLock().unlock(); | ||
cleanerThreadLock.writeLock().lock(); | ||
try { | ||
if (trackedObjects.get() == 0) { | ||
Logger.getLogger(Cleaner.class.getName()).log(Level.FINE, "Shutting down CleanerThread"); | ||
cleanerThread = null; | ||
break; | ||
} | ||
registeredCleaners.append(cleanerRef.cleanupTask.toString()); | ||
} finally { | ||
cleanerThreadLock.readLock().lock(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. Re-locking can be avoided. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. |
||
cleanerThreadLock.writeLock().unlock(); | ||
} | ||
logger.log(Level.FINER, "Registered Cleaners: {0}", registeredCleaners.toString()); | ||
} | ||
} finally { | ||
cleanerThreadLock.readLock().unlock(); | ||
} | ||
Logger logger = Logger.getLogger(Cleaner.class.getName()); | ||
if (logger.isLoggable(Level.FINER)) { | ||
logger.log(Level.FINER, "Registered Cleaners: {0}", trackedObjects.get()); | ||
} | ||
} | ||
} catch (InterruptedException ex) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the readlock is not necessary at all.
trackedObjects
is anAtomicLong
and doesn't need any protection.The important step is to protect the starting and stopping of the CleanerThread. In particular, it must be ensured that the thread doesn't terminate while a new reference is added without starting a new CleanerThread..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
cleanerThreadLock
protects the interaction oftrackedObjects
andcleanerThread
. The critical section is the starting and stopping of the Thead if and only iff the number of tracked objects reaches zero or is above 1.This is the sequence I want to prevent:
T1
using the cleaner callsregister
and runs beforeincrementAndGet
is invokedCleanerThread
reaches thetrackedObjects.get() == 0
check and enters theif
block, but does not execute further yet.T1
executesincrementAndGet
and receives1
, checks the value ofcleanerThread
and finds aThread
there.CleanerThread
continues, clearscleanerThread
and finishes executionThere is a subtle issue there, the
break
needs to move into the innerif
in line 173 (https://github.com/java-native-access/jna/pull/1617/files/b901c180f4a1026675a2859bbf24727574e19e6a#diff-11b3b0981223743529d972f317f26dfd9488b4a9b80a34a2d6f76cc2c58dcc00R173) into the inner if.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I attempted to write some pseudocode to show how to do it with a single lock and found a race in it. The cause of the race is the double-check locking that you employ in order to achieve better parallelism.
Without the double-check a single lock is sufficient, but then you're back to a mutex block that is always executed, hence no more real parallelism.
I still wonder if a tiny mutex block would be faster than the read/write lock, in particular because the ReadWriteLock JavaDoc says just that: