How to Preserve Trace Context Across Asynchronous Java Threads with TransmittableThreadLocal
This article explains why ThreadLocal‑based trace information is lost in asynchronous Java calls, compares InheritableThreadLocal and the Alibaba‑provided TransmittableThreadLocal library, shows how to wrap runnables with TtlRunnable, and demonstrates a Java Agent solution for transparent thread‑pool propagation.
ThreadLocal Trace Loss in Asynchronous Calls
In full‑link tracing frameworks, trace information is stored in a ThreadLocal. When business logic uses asynchronous calls, the trace is lost because the new thread does not inherit the original ThreadLocal value, breaking the call chain.
ThreadLocal<String> traceContext = new ThreadLocal<>();
String traceId = Tracer.startServer();
traceContext.set(traceId);
... // use traceContext in the same thread
Tracer.startClient(traceContext.get());
Tracer.endClient();
Tracer.endServer();If the next span runs in an async thread, it cannot obtain the previous span’s trace ID.
InheritableThreadLocal
JDK provides InheritableThreadLocal to copy values from a parent thread to a child thread. When a child thread is created, the JVM shallow‑copies the parent’s ThreadLocalMap into the child.
if (parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}The copy is shallow: keys and values reference the same objects, so a child thread can overwrite the parent’s value. This works only for true parent‑child thread relationships, not for thread‑pool threads that are reused.
TransmittableThreadLocal
TransmittableThreadLocal (TTL) is an Alibaba open‑source library that extends InheritableThreadLocal and adds support for thread‑pool reuse. It provides TtlRunnable and TtlCallable wrappers that capture the current thread’s ThreadLocal map, store it, and restore it when the task runs in another thread.
private final AtomicReference<Object> capturedRef;
private final Runnable runnable;
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(Runnable runnable, boolean release) {
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = release;
} @Nonnull
public static Object capture() {
Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<>();
for (TransmittableThreadLocal<?> tl : holder.get().keySet()) {
captured.put(tl, tl.copyValue());
}
return captured;
} public void run() {
Object captured = capturedRef.get();
if (captured == null || (releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null))) {
throw new IllegalStateException("TTL value reference is released after run!");
}
Object backup = replay(captured);
try {
runnable.run();
} finally {
restore(backup);
}
}When a task is submitted to a thread pool, TTL restores the captured values so the child thread sees the same trace ID as the parent.
public void testAsync() {
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executorService);
String traceId = Tracer.startServer();
ThreadLocal<String> traceContext = new TransmittableThreadLocal<>();
traceContext.set(traceId);
ttlExecutor.submit(new Runnable() {
@Override
public void run() {
String childTraceId = traceContext.get();
Assert.assertEquals(childTraceId, traceId);
Tracer.startClient(traceId);
Tracer.endClient();
}
});
Tracer.endServer();
}Java Agent for Transparent Injection
Modifying every thread‑pool submission is intrusive. A Java Agent (Instrumentation) can weave the TTL logic into the bytecode at runtime, making the propagation transparent to application code. The agent inserts code into executor methods to retrieve and set captured ThreadLocal values.
private static void updateMethodOfExecutorClass(final CtClass clazz, final CtMethod method)
throws NotFoundException, CannotCompileException {
if (method.getDeclaringClass() != clazz) return;
int modifiers = method.getModifiers();
if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers)) return;
CtClass[] parameterTypes = method.getParameterTypes();
StringBuilder insertCode = new StringBuilder();
for (int i = 0; i < parameterTypes.length; i++) {
CtClass paraType = parameterTypes[i];
if (RUNNABLE_CLASS_NAME.equals(paraType.getName())) {
String code = String.format("$%d = %s.get($%d, false, true);", i + 1, TTL_RUNNABLE_CLASS_NAME, i + 1);
insertCode.append(code);
} else if (CALLABLE_CLASS_NAME.equals(paraType.getName())) {
String code = String.format("$%d = %s.get($%d, false, true);", i + 1, TTL_CALLABLE_CLASS_NAME, i + 1);
insertCode.append(code);
}
}
if (insertCode.length() > 0) {
method.insertBefore(insertCode.toString());
}
}Package the TTL jar, place it in a directory (e.g., agent/), and start the JVM with:
-javaagent:agent/transmittable-thread-local-xxx.jarThis injects the necessary bytecode without changing application source.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITFLY8 Architecture Home
ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
