Introduction of TTL

Introduction to ThreadLocal and its underlying principles

[InheritableThreadLocal] [InheritableThreadLocal] [InheritableThreadLocal] [InheritableThreadLocal

In the previous article, we covered the mechanisms of ThreadLocal (TL) and InheritableThreadLocal (ITL), as well as their pros and cons. At the end of the ITL article, we learned that in the case of thread pools, ITL does not have a good solution for asynchronous out-of-the-box delivery because its mechanism of action depends on the thread Init method.

Is there a solution to this problem under thread pools?

The affirmative answer is yes!

There is a TransmittableThreadLocal that we need to introduce next.

In ITL, the type of DemoContext is changed to TransmittableThreadLocal, and the thread pool is wrapped in the way provided by TTL to achieve the corresponding modification.

    @SneakyThrows
    public Boolean testThreadLocal(String s){
        LOGGER.info("The actual value passed in is:" + s);
        DemoContext.setContext(Integer.valueOf(s)); // The DemoContext must also be set to TransmittableThreadLocal
        CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
            try{
                LOGGER.info(String.format("Child thread ID =%s, contextStr =%s"
                                          ,Thread.currentThread().getId(),DemoContext.getContext()));
            }catch (Throwable throwable){
                return throwable;
            }
            return null;
        },demoExecutor); // demoExecutor needs to use the thread pool class provided by TTL
        LOGGER.info(String.format("Main thread ID =%s, contextStr: %s"
                                  ,Thread.currentThread().getId(),DemoContext.getContext()));
        Throwable throwable = subThread.get();
        if(throwable! =null) {throw throwable;
        }
        DemoContext.clearContext();
        return true; }...@Bean(name = "demoExecutor")
    public Executor demoExecutor(a) {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setTaskDecorator(new GatewayHeaderTaskDecorator());
        threadPoolTaskExecutor.setCorePoolSize(5);
        threadPoolTaskExecutor.setQueueCapacity(0);
        threadPoolTaskExecutor.setKeepAliveSeconds(3);
        threadPoolTaskExecutor.setMaxPoolSize(50);
        threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(false);
        threadPoolTaskExecutor.initialize();
        // Use TTL to wrap the corresponding thread pool
        return TtlExecutors.getTtlExecutor(threadPoolTaskExecutor.getThreadPoolExecutor());
    }
Copy the code

You can see the corresponding result as follows:

It is obvious that the TransmittableThreadLocal implements normal value passing even when using thread pools.

For more information, such as creating threads without using a thread pool, refer to the official TTL documentation.

The underlying principle

So how is TTL implemented at the bottom? Let us look from the source step by step ~

First, obviously, the secret to TTL’s implementation capabilities lies in how it wraps thread pools, and here’s how it wraps thread pools. GetTtlExecutor: getTtlExecutor

@Nullable
public static Executor getTtlExecutor(@Nullable Executor executor) {
    if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) {
        return executor;
    }
    return new ExecutorTtlWrapper(executor, true);
}

Copy the code

As you can see, this simply returns a wrapped thread pool, so we’ll have to dig a little deeper. Find the corresponding thread pool class ExecutorTtlWrapper and take a look at the excute method for thread pools:

class ExecutorTtlWrapper implements Executor.TtlWrapper<Executor>, TtlEnhanced {...@Override
    public void execute(@NonNull Runnable command) {
        executor.execute(TtlRunnable.get(command, false, idempotent)); }... }Copy the code

Huh? This ** ttlrunnable.get ()** clearly encapsulates the Runnable task we need to perform. Follow up source code:

@Nullable
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, 
                              boolean idempotent) {
    if (null == runnable) return null;

    if (runnable instanceof TtlEnhanced) {
        // avoid redundant decoration, and ensure idempotency
        if (idempotent) return (TtlRunnable) runnable;
        else throw new IllegalStateException("Already TtlRunnable!");
    }
    return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}
Copy the code

Huh? Why another layer of nesting dolls? (the heart dark feeling is not good), in order to find the actual mystery, had to be hard to follow in. Following the TtlRunnable class, we can find our key method run () and key constructor. Of particular note is the **capture () ** method in the constructor.

public final class TtlRunnable {
    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.capturedRef = new AtomicReference<Object>(capture()); // Key code
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; }}Copy the code

Capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture () : capture ()

@NonNull
public static Object capture(a) {
    // Generate a snapshot
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

private static class Snapshot {
    final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
    final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;

    private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
        this.ttl2Value = ttl2Value;
        this.threadLocal2Value = threadLocal2Value; }}Copy the code

At this point, the whole asynchronous variable implementation principle has been completed, very simple to think of, but in fact in the reasonable. However, ttlRunable’s run method has another subtle point that I should introduce to you

    @Override
    public void run(a) {
        final Object captured = capturedRef.get(); // Obtain all TTL and TL snapshots
        if (captured == null|| releaseTtlValueReferenceAfterRun && ! capturedRef.compareAndSet(captured,null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        final Object backup = replay(captured);// Get a backup of the thread
        try {
            runnable.run(); // Execute the task of the thread
        } finally {
            restore(backup); // Then restore the task of the thread to its original backup state}}Copy the code

At first, I was very confused about this place. Why do we need to do replay operation duck?

Until I read the original author’s comments, combined with some of my own understanding and thinking, I came to the following conclusions:

The main reason is that when the maximum number of threads in the thread pool is determined and the rejection policy of the thread pool is CallerRunsPolicy, the program executed twice may be the business thread itself. If the replay mechanism is not adopted and the content of TTL is modified during the process, there will be problems.

The following figure shows the execution mode of the parent and child threads under normal circumstances. At this time, since the data of the parent and child threads are separated, the child thread can modify the contents of the TTL at will without affecting the logic of the original thread.

However, if the maximum number of threads in the thread pool and the blocking queue are slow during peak business hours, and the callerRunsPolicy reject policy is used, then the task execution diagram might look like this.

Therefore, the mechanism of capturing snapshots is adopted. In addition to the design mentioned above, there are many neat designs in TTL, such as the Holder variable that holds TTL data for each thread. Here is limited to the reason of space, it is just a simple introduction ~

    // Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap
      
       , ? >.
      
    WeakHashMap is used as a *Set*:
    // the value of WeakHashMap is *always* null, and never used.
    // 2.2 WeakHashMap support *null* value.
	// Presumably, the holder is where the TTL for each thread is stored
    private static finalInheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ? >> holder =newInheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ? > > () {@Override
                protectedWeakHashMap<TransmittableThreadLocal<Object>, ? > initialValue() {// Create a set that starts with null
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
                }

                @Override
                protectedWeakHashMap<TransmittableThreadLocal<Object>, ? > childValue(WeakHashMap<TransmittableThreadLocal<Object>, ? > parentValue) {// Copy my dad's TTL
                    return newWeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue); }};Copy the code

conclusion

In general, TTL moves the timing of transmission of asynchronous thread variables from the time the thread initially creates to the time the thread task executes. This ensures that thread variables are passed down even when thread pools are in use.

In addition, the thread variable snapshot and replay mechanism is adopted to avoid the business data disorder that may occur in the case of high concurrency, which is a very sophisticated design.

If you see this, you might as well give me a like, click my favorite, and if you can also follow a little bit better

Creation is not easy, thank you for your support

reference

TTL Official document