Skip to content

Commit 6766ecf

Browse files
committed
Make annotation caching thread-safe
Only one thread at a time is now allowed to cache annotations for a given class. Closes #148
1 parent 8b872ee commit 6766ecf

File tree

1 file changed

+73
-50
lines changed

1 file changed

+73
-50
lines changed

src/main/java/org/scijava/util/ClassUtils.java

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -426,64 +426,87 @@ public static <A extends Annotation> void getAnnotatedFields(
426426
public static void cacheAnnotatedObjects(final Class<?> scannedClass,
427427
final Query query)
428428
{
429-
// NB: The java.lang.Object class does not have any annotated methods.
430-
// And even if it did, it definitely does not have any methods annotated
431-
// with SciJava annotations such as org.scijava.event.EventHandler, which
432-
// are the main sorts of methods we are interested in.
433-
if (scannedClass == null || scannedClass == Object.class) return;
434-
435-
// Initialize step - determine which queries are solved
436-
final Set<Class<? extends Annotation>> keysToDrop =
437-
new HashSet<Class<? extends Annotation>>();
438-
for (final Class<? extends Annotation> annotationClass : query.keySet()) {
439-
// Fields
440-
if (fieldCache.getList(scannedClass, annotationClass) != null) {
441-
keysToDrop.add(annotationClass);
442-
}
443-
else if (methodCache.getList(scannedClass, annotationClass) != null) {
444-
keysToDrop.add(annotationClass);
429+
// Only allow one thread at a time to populate cached objects for a given
430+
// class. This lock is technically overkill - the minimal lock would be
431+
// on the provided Query contents + class. Devising a way to obtain this
432+
// lock could be useful - as if Query A and Query B were executed on the
433+
// same class by different threads, there are three scenarios:
434+
// 1) intersection of A + B is empty - then they can run on separate threads
435+
// 2) A == B - whichever was received second must wait for the first to
436+
// finish.
437+
// 3) A != B and intersection of A + B is not empty - the intersection subset
438+
// can be safely performed on a separate thread, but the later query must
439+
// still wait for the earlier query to complete.
440+
//
441+
// NB: an alternative would be to update the getAnnotatedxxx methods to
442+
// return Sets instead of Lists. Then threads can pretty much go nuts
443+
// as long as you double lock the Set creation in a synchronized block.
444+
//
445+
// NB: another possibility would be to keep this synchronized entry point
446+
// but divide the work for each Query into asynchronous blocks. However, it
447+
// has not been investigated how much of a performance boost that would
448+
// provide as it would then cause multiple traversals of the class hierarchy
449+
// - which is exactly what the Query notation was created to avoid.
450+
synchronized (scannedClass) {
451+
// NB: The java.lang.Object class does not have any annotated methods.
452+
// And even if it did, it definitely does not have any methods annotated
453+
// with SciJava annotations such as org.scijava.event.EventHandler, which
454+
// are the main sorts of methods we are interested in.
455+
if (scannedClass == null || scannedClass == Object.class) return;
456+
457+
// Initialize step - determine which queries are solved
458+
final Set<Class<? extends Annotation>> keysToDrop =
459+
new HashSet<Class<? extends Annotation>>();
460+
for (final Class<? extends Annotation> annotationClass : query.keySet()) {
461+
// Fields
462+
if (fieldCache.getList(scannedClass, annotationClass) != null) {
463+
keysToDrop.add(annotationClass);
464+
}
465+
else if (methodCache.getList(scannedClass, annotationClass) != null) {
466+
keysToDrop.add(annotationClass);
467+
}
445468
}
446-
}
447469

448-
// Clean up resolved keys
449-
for (final Class<? extends Annotation> key : keysToDrop) {
450-
query.remove(key);
451-
}
470+
// Clean up resolved keys
471+
for (final Class<? extends Annotation> key : keysToDrop) {
472+
query.remove(key);
473+
}
452474

453-
// Stop now if we know all requested information is cached
454-
if (query.isEmpty()) return;
475+
// Stop now if we know all requested information is cached
476+
if (query.isEmpty()) return;
455477

456-
final List<Class<?>> inherited = new ArrayList<Class<?>>();
478+
final List<Class<?>> inherited = new ArrayList<Class<?>>();
457479

458-
// cache all parents recursively
459-
final Class<?> superClass = scannedClass.getSuperclass();
460-
if (superClass != null) {
461-
// Recursive step
462-
cacheAnnotatedObjects(superClass, new Query(query));
463-
inherited.add(superClass);
464-
}
480+
// cache all parents recursively
481+
final Class<?> superClass = scannedClass.getSuperclass();
482+
if (superClass != null) {
483+
// Recursive step
484+
cacheAnnotatedObjects(superClass, new Query(query));
485+
inherited.add(superClass);
486+
}
465487

466-
// cache all interfaces recursively
467-
for (final Class<?> ifaceClass : scannedClass.getInterfaces()) {
468-
// Recursive step
469-
cacheAnnotatedObjects(ifaceClass, new Query(query));
470-
inherited.add(ifaceClass);
471-
}
488+
// cache all interfaces recursively
489+
for (final Class<?> ifaceClass : scannedClass.getInterfaces()) {
490+
// Recursive step
491+
cacheAnnotatedObjects(ifaceClass, new Query(query));
492+
inherited.add(ifaceClass);
493+
}
472494

473-
// Populate supported objects for scanned class
474-
for (final Class<? extends Annotation> annotationClass : query.keySet()) {
475-
final Class<? extends AnnotatedElement> objectClass =
476-
query.get(annotationClass);
495+
// Populate supported objects for scanned class
496+
for (final Class<? extends Annotation> annotationClass : query.keySet()) {
497+
final Class<? extends AnnotatedElement> objectClass =
498+
query.get(annotationClass);
477499

478-
// Methods
479-
if (Method.class.isAssignableFrom(objectClass)) {
480-
populateCache(scannedClass, inherited, annotationClass, methodCache,
481-
scannedClass.getDeclaredMethods());
482-
}
483-
// Fields
484-
else if (Field.class.isAssignableFrom(objectClass)) {
485-
populateCache(scannedClass, inherited, annotationClass, fieldCache,
486-
scannedClass.getDeclaredFields());
500+
// Methods
501+
if (Method.class.isAssignableFrom(objectClass)) {
502+
populateCache(scannedClass, inherited, annotationClass, methodCache,
503+
scannedClass.getDeclaredMethods());
504+
}
505+
// Fields
506+
else if (Field.class.isAssignableFrom(objectClass)) {
507+
populateCache(scannedClass, inherited, annotationClass, fieldCache,
508+
scannedClass.getDeclaredFields());
509+
}
487510
}
488511
}
489512
}

0 commit comments

Comments
 (0)