blob: 3ac4b52ac925c828defdbc5c63c27efa4c27368a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.commons.threads.impl;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.util.Arrays;
import org.apache.sling.commons.threads.impl.ThreadLocalChangeListener.Mode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Notifies a {@link ThreadLocalChangeListener} about changes on a thread local storage. In addition it removes all references to variables
* being added to the thread local storage while the cleaner was running with its {@link cleanup} method.
*
* @see <a href="http://www.javaspecialists.eu/archive/Issue229.html">JavaSpecialist.eu - Cleaning ThreadLocals</a> */
public class ThreadLocalCleaner {
private static final Logger LOG = LoggerFactory.getLogger(ThreadLocalCleaner.class);
/* Reflection fields */
/** this field is in class {@link ThreadLocal} and is of type {@code ThreadLocal.ThreadLocalMap} */
private static final Field threadLocalsField;
/** this field is in class {@link ThreadLocal} and is of type {@code ThreadLocal.ThreadLocalMap} */
private static final Field inheritableThreadLocalsField;
private static final Class<?> threadLocalMapClass;
/** this field is in class {@code ThreadLocal.ThreadLocalMap} and contains an array of {@code ThreadLocal.ThreadLocalMap.Entry's} */
private static final Field tableField;
private static final Class<?> threadLocalMapEntryClass;
/** this field is in class {@code ThreadLocal.ThreadLocalMap.Entry} and contains an object referencing the actual thread local
* variable */
private static final Field threadLocalEntryValueField;
/** this field is in the class {@code ThreadLocal.ThreadLocalMap} and contains the number of the entries */
private static final Field threadLocalMapSizeField;
/** this field is in the class {@code ThreadLocal.ThreadLocalMap} and next resize threshold */
private static final Field threadLocalMapThresholdField;
static {
try {
threadLocalsField = field(Thread.class, "threadLocals");
inheritableThreadLocalsField = field(Thread.class, "inheritableThreadLocals");
threadLocalMapClass = inner(ThreadLocal.class, "ThreadLocalMap");
tableField = field(threadLocalMapClass, "table");
threadLocalMapEntryClass = inner(threadLocalMapClass, "Entry");
threadLocalEntryValueField = field(threadLocalMapEntryClass, "value");
threadLocalMapSizeField = field(threadLocalMapClass, "size");
threadLocalMapThresholdField = field(threadLocalMapClass, "threshold");
} catch (NoSuchFieldException e) {
ExceptionInInitializerError error = new ExceptionInInitializerError(
"Unable to access ThreadLocal class information using reflection");
error.initCause(e);
throw error;
}
}
/** @param c the class containing the field
* @param name the name of the field
* @return the field from the given class with the given name (made accessible)
* @throws NoSuchFieldException */
private static Field field(Class<?> c, String name)
throws NoSuchFieldException {
Field field = c.getDeclaredField(name);
field.setAccessible(true);
return field;
}
/** @param clazz the class containing the inner class
* @param name the name of the inner class
* @return the class with the given name, declared as inner class of the given class */
private static Class<?> inner(Class<?> clazz, String name) {
for (Class<?> c : clazz.getDeclaredClasses()) {
if (c.getSimpleName().equals(name)) {
return c;
}
}
throw new IllegalStateException(
"Could not find inner class " + name + " in " + clazz);
}
private static Reference<?>[] copy(Field field) {
try {
Thread thread = Thread.currentThread();
Object threadLocals = field.get(thread);
if (threadLocals == null)
return null;
Reference<?>[] table = (Reference<?>[]) tableField.get(threadLocals);
return Arrays.copyOf(table, table.length);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access denied", e);
}
}
private static Integer size(Field field, Field sizeField) {
try {
Thread thread = Thread.currentThread();
Object threadLocals = field.get(thread);
if (threadLocals == null)
return null;
return (Integer) sizeField.get(threadLocals);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access denied", e);
}
}
private static void restore(Field field, Object[] value, Integer size, Integer threshold) {
try {
Thread thread = Thread.currentThread();
if (value == null) {
field.set(thread, null);
LOG.debug("Restored {} to a null value", field.getName());
} else {
final Object threadLocals = field.get(thread);
tableField.set(threadLocals, value);
threadLocalMapSizeField.set(threadLocals, size);
threadLocalMapThresholdField.set(threadLocals, threshold);
LOG.debug("Restored {} with to {} references, size {}, threshold {}" ,field.getName(), value.length, size, threshold);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access denied", e);
}
}
private final ThreadLocalChangeListener listener;
private ThreadLocalMapCopy threadLocalsCopy;
private ThreadLocalMapCopy inheritableThreadLocalsCopy;
public ThreadLocalCleaner(ThreadLocalChangeListener listener) {
this.listener = listener;
saveOldThreadLocals();
}
public void cleanup() {
// the first two diff calls are only to notify the listener, the actual cleanup is done by restoreOldThreadLocals
diff(threadLocalsField, threadLocalsCopy.references);
diff(inheritableThreadLocalsField, inheritableThreadLocalsCopy.references);
restoreOldThreadLocals();
}
/** Notifies the {@link ThreadLocalChangeListener} about changes on thread local variables for the current thread.
*
* @param field is a field containing a ThreadLocalMap
* @param backup */
private void diff(Field field, Reference<?>[] backup) {
try {
Thread thread = Thread.currentThread();
Object threadLocals = field.get(thread);
if (threadLocals == null) {
if (backup != null) {
for (Reference<?> reference : backup) {
changed(thread, reference, Mode.REMOVED);
}
}
return;
}
Reference<?>[] current = (Reference<?>[]) tableField.get(threadLocals);
if (backup == null) {
for (Reference<?> reference : current) {
changed(thread, reference, Mode.ADDED);
}
} else {
// nested loop - both arrays *should* be relatively small
next: for (Reference<?> curRef : current) {
if (curRef != null) {
if (curRef.get() == this.threadLocalsCopy ||
curRef.get() == this.inheritableThreadLocalsCopy) {
continue next;
}
for (Reference<?> backupRef : backup) {
if (curRef == backupRef)
continue next;
}
// could not find it in backup - added
changed(thread, curRef, Mode.ADDED);
}
}
next: for (Reference<?> backupRef : backup) {
for (Reference<?> curRef : current) {
if (curRef == backupRef)
continue next;
}
// could not find it in current - removed
changed(thread, backupRef, Mode.REMOVED);
}
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access denied", e);
}
}
private void changed(Thread thread, Reference<?> reference,
ThreadLocalChangeListener.Mode mode)
throws IllegalAccessException {
// just skip null reference entries (may happen if array has been resized)
if (reference != null) {
listener.changed(mode,
thread, (ThreadLocal<?>) reference.get(),
threadLocalEntryValueField.get(reference));
}
}
private void saveOldThreadLocals() {
threadLocalsCopy = new ThreadLocalMapCopy(copy(threadLocalsField),
size(threadLocalsField, threadLocalMapSizeField),
size(threadLocalsField, threadLocalMapThresholdField));
threadLocalsCopy.debug("saved", "Thread locals");
inheritableThreadLocalsCopy = new ThreadLocalMapCopy(copy(inheritableThreadLocalsField),
size(inheritableThreadLocalsField, threadLocalMapSizeField),
size(inheritableThreadLocalsField, threadLocalMapThresholdField));
inheritableThreadLocalsCopy.debug("saved", "Inheritable thread locals");
}
private void restoreOldThreadLocals() {
try {
restore(inheritableThreadLocalsField, inheritableThreadLocalsCopy.references,
inheritableThreadLocalsCopy.size, inheritableThreadLocalsCopy.threshold);
restore(threadLocalsField, threadLocalsCopy.references,
threadLocalsCopy.size, threadLocalsCopy.threshold);
} finally {
threadLocalsCopy = null;
inheritableThreadLocalsCopy = null;
}
}
/**
* Helper class that encapsulates the state from a <tt>ThreadLocalMap</tt>
*
*/
static class ThreadLocalMapCopy {
private final Reference<?>[] references;
private final Integer size;
private final Integer threshold;
private ThreadLocalMapCopy(Reference<?>[] references, Integer size, Integer threshold) {
this.references = references;
this.size = size;
this.threshold = threshold;
}
void debug(String event, String mapName) {
if ( references != null ) {
ThreadLocalCleaner.LOG.debug("{}: {} {} references, size: {}, threshold: {}",
mapName, event, references.length, size, threshold);
} else {
ThreadLocalCleaner.LOG.debug("{}: {} null references", mapName, event);
}
}
}
}