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.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
How to Preserve Trace Context Across Asynchronous Java Threads with TransmittableThreadLocal

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.jar

This injects the necessary bytecode without changing application source.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadLocalInheritableThreadLocalTransmittableThreadLocal
ITFLY8 Architecture Home
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.