How to Implement Link Tracking Starting from ThreadLocal

1. What is ThreadLocal?ThreadLocal allows each thread to have its own variable copy, which does not interfere with each other.Usage example:

public class BasicExample {    // Create a ThreadLocal variable    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    public static void main(String[] args) {        // Set value in the main thread        threadLocal.set(10);        System.out.println("Main thread gets value: " + threadLocal.get()); // Output 10
        // Get value in a new thread (initial value)        new Thread(() -> {            System.out.println("Child thread gets value: " + threadLocal.get()); // Output 0        }).start();    }}

Implementation principle:

Each Thread object has an instance of <span><span>ThreadLocal.ThreadLocalMap</span></span> (which can be understood as a Map exclusive to that thread). When you call <span><span>threadLocal.set(value)</span></span>, the following happens:

  1. Get the current thread

  2. Get the ThreadLocalMap of that thread

  3. Store the value with the ThreadLocal instance as the key

Using ThreadLocal to implement link tracking allows multiple method calls without passing values, obtaining the TraceId through ThreadLocal, as shown below:

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {        threadLocal.set("traceId-1");        doSomething();        doSomething2();        System.out.println(threadLocal.get());    }
    private static void doSomething() {        System.out.println(threadLocal.get());    }
    private static void doSomething2(){        System.out.println(threadLocal.get());    }}

The output is as follows:

"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=56510:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;D:\code\HelloWorld\target\classes;C:\Users\mjb-7\.m2\repository\com\alibaba\transmittable-thread-local\2.14.2\transmittable-thread-local-2.14.2.jar" org.example.ThreadLocalExampletraceId-1traceId-1traceId-1

However, the problem arises when dealing with multithreading, as shown below:

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "initial");
    public static void main(String[] args) {        threadLocal.set("traceId-1");        doSomething();        doSomething2();        System.out.println(threadLocal.get());
        new Thread(() ->{            System.out.println("In thread -" + threadLocal.get());        }).start();
        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }    }
    private static void doSomething() {        System.out.println(threadLocal.get());    }
    private static void doSomething2(){        System.out.println(threadLocal.get());    }}

The output is as follows:

"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=56808:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;D:\code\HelloWorld\target\classes;C:\Users\mjb-7\.m2\repository\com\alibaba\transmittable-thread-local\2.14.2\transmittable-thread-local-2.14.2.jar" org.example.ThreadLocalExampletraceId-1traceId-1traceId-1In thread -initial

As we can see, the new thread retrieves the initial value and does not get the value from the main thread. How can we solve this? By introducing InheritableThreadLocal.

2. InheritableThreadLocal

InheritableThreadLocal extends ThreadLocal, allowing child threads to inherit values from the parent thread.

Implementation principle:In the constructor of Thread, it checks if the parent thread’s inheritableThreadLocals exist, and if so, it copies them.

Replacing the previous example with InheritableThreadLocal:

public class ThreadLocalExample {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<String>(){        @Override        protected String initialValue() {            return "initial";        }    };
    public static void main(String[] args) {        threadLocal.set("traceId-1");        doSomething();        doSomething2();        System.out.println(threadLocal.get());
        new Thread(() ->{            System.out.println("In thread -" + threadLocal.get());        }).start();
        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }    }
    private static void doSomething() {        System.out.println(threadLocal.get());    }
    private static void doSomething2(){        System.out.println(threadLocal.get());    }}

The output is as follows:

"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=56972:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;D:\code\HelloWorld\target\classes;C:\Users\mjb-7\.m2\repository\com\alibaba\transmittable-thread-local\2.14.2\transmittable-thread-local-2.14.2.jar" org.example.ThreadLocalExampletraceId-1traceId-1traceId-1In thread -traceId-1

However, InheritableThreadLocal only copies values when creating child threads; after that, the values of parent and child threads become independent, which can also cause issues in thread pools.

public class ThreadLocalExample {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<String>(){        @Override        protected String initialValue() {            return "initial";        }    };
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
    public static void main(String[] args) throws InterruptedException {        threadLocal.set("traceId-1");        System.out.println(threadLocal.get());
        Thread t1 = new Thread(() ->{            System.out.println("Thread 1 -" + threadLocal.get());        });
        t1.start();        t1.join();
        executorService.execute(() ->{            System.out.println("Thread pool 1 -" + threadLocal.get());        });
        threadLocal.set("traceId-2");
        executorService.execute(() ->{            System.out.println("Thread pool 2 -" + threadLocal.get());        });
        Thread t2 = new Thread(() ->{            System.out.println("Thread 2 -" + threadLocal.get());        });
        t2.start();        t2.join();    }}

The output is as follows, showing that in the thread pool, multiple executions still retrieve the first value inserted.

"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=57231:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;D:\code\HelloWorld\target\classes;C:\Users\mjb-7\.m2\repository\com\alibaba\transmittable-thread-local\2.14.2\transmittable-thread-local-2.14.2.jar" org.example.ThreadLocalExampletraceId-1Thread 1 -traceId-1Thread pool 1 -traceId-1Thread pool 2 -traceId-1Thread 2 -traceId-2

How to solve this? By introducing Alibaba’s open-source TransmittableThreadLocal.

3. TransmittableThreadLocal

Add dependency:

<!-- Maven --><dependency>    <groupId>com.alibaba</groupId>    <artifactId>transmittable-thread-local</artifactId>    <version>2.14.2</version> <!-- Use the latest version --></dependency>

Modify the previous example as follows:

public class ThreadLocalExample {
    private static TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>(){        @Override        protected String initialValue() {            return "initial";        }    };
    private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
    public static void main(String[] args) throws InterruptedException {        threadLocal.set("traceId-1");        System.out.println(threadLocal.get());
        Thread t1 = new Thread(() ->{            System.out.println("Thread 1 -" + threadLocal.get());        });
        t1.start();        t1.join();
        executorService.execute(() ->{            System.out.println("Thread pool 1 -" + threadLocal.get());        });
        threadLocal.set("traceId-2");
        executorService.execute(() ->{            System.out.println("Thread pool 2 -" + threadLocal.get());        });
        Thread t2 = new Thread(() ->{            System.out.println("Thread 2 -" + threadLocal.get());        });
        t2.start();        t2.join();    }}

The output is as follows:

"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=57619:C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;D:\code\HelloWorld\target\classes;C:\Users\mjb-7\.m2\repository\com\alibaba\transmittable-thread-local\2.14.2\transmittable-thread-local-2.14.2.jar" org.example.ThreadLocalExampletraceId-1Thread 1 -traceId-1Thread pool 1 -traceId-1Thread pool 2 -traceId-2Thread 2 -traceId-2

As we can see, both thread 2 and thread pool 2 receive the latest set value.

Implementation principle:

TransmittableThreadLocal achieves this by wrapping Runnable/Callable:

  1. When a task is submitted, capture all TransmittableThreadLocal values of the current thread

  2. Before executing the task, set these values in the executing thread

  3. After the task execution is complete, restore the original values of the executing thread

That’s all for today. If you found this helpful, please like and bookmark. I am OutOfMemory, and feel free to follow me for more articles.

Leave a Comment