首页 > 编程语言 >并发编程 (TransmittableThreadLocal)

并发编程 (TransmittableThreadLocal)

时间:2024-09-14 14:48:19浏览次数:9  
标签:变量 Thread get 变量值 编程 并发 线程 TransmittableThreadLocal 名称

TransmittableThreadLocal的使用及原理解析

一、基本使用

首先,TTL是用来解决ITL解决不了的问题而诞生的,所以TTL一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL也可以做到,但是前面有讲过,ITL线程池的模式下,就没办法再正确传递了,所以TTL做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去,先来看个例子:

// 需要注意的是,使用TTL的时候,要想传递的值不出问题,线程池必须得用TTL加一层代理(下面会讲这样做的目的)
    private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));

    private static ThreadLocal tl = new TransmittableThreadLocal<>(); //这里采用TTL的实现

    public static void main(String[] args) {

        new Thread(() -> {

            String mainThreadName = "main_01";

            tl.set(1);

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            sleep(1L); //确保上面的会在tl.set执行之前执行
            tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));

        }).start();


        new Thread(() -> {

            String mainThreadName = "main_02";

            tl.set(3);

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            sleep(1L); //确保上面的会在tl.set执行之前执行
            tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            });

            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));

        }).start();

    }

    private static void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果:

线程名称-Thread-2, 变量值=4
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=3
线程名称-Thread-1, 变量值=2
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=1
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=1
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=3
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=3
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=1
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=2
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=4
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=2

程序有些啰嗦,为了说明问题,加了很多说明,但至少通过上面的例子,不难发现,两个主线程里都使用线程池异步,而且值在主线程里还发生过改变,测试结果展示一切正常,由此可以知道TTL在使用线程池的情况下,也可以很好的完成传递,而且不会发生错乱。

那么是不是对普通线程异步也有这么好的支撑呢?

改造下上面的测试代码:

private static ThreadLocal tl = new TransmittableThreadLocal<>();

    public static void main(String[] args) {

        new Thread(() -> {

            String mainThreadName = "main_01";

            tl.set(1);

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            sleep(1L); //确保上面的会在tl.set执行之前执行
            tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));

        }).start();


        new Thread(() -> {

            String mainThreadName = "main_02";

            tl.set(3);

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            sleep(1L); //确保上面的会在tl.set执行之前执行
            tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            new Thread(() -> {
                sleep(1L);
                System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
            }).start();

            System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));

        }).start();
    }

相比代码块1,这一段的异步全都是普通异步,未采用线程池的方式进行异步,看下运行结果:

本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-14, 变量值=4
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-5, 变量值=1
线程名称-Thread-1, 变量值=2
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-3, 变量值=1
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-11, 变量值=2
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-6, 变量值=3
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-12, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-10, 变量值=4
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-8, 变量值=3
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-4, 变量值=3
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-7, 变量值=1
线程名称-Thread-2, 变量值=4
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-9, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-13, 变量值=2

ok,可以看到,达到了跟第一个测试一致的结果。

到这里,通过上述两个例子,TTL的基本使用,以及其解决的问题,我们已经有了初步的了解,下面我们来解析一下其内部原理,看看TTL是怎么完成对ITL的优化的。

二、原理分析

先来看TTL里面的几个重要属性及方法

TTL定义:

public class TransmittableThreadLocal extends InheritableThreadLocal

可以看到,TTL继承了ITL,意味着TTL首先具备ITL的功能。

再来看看一个重要属性holder

/**
   * 这是一个ITL类型的对象,持有一个全局的WeakMap(weakMap的key是弱引用,同TL一样,也是为了解决内存泄漏的问题),里面存放了TTL对象
   * 并且重写了initialValue和childValue方法,尤其是childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的
   */
  private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
          new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
              @Override
              protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
                  return new WeakHashMap<TransmittableThreadLocal<?>, Object>();
              }

              @Override
              protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
                  return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
              }
          };

再来看下setget

//下面的方法均属于TTL类
@Override
    public final void set(T value) {
        super.set(value);
        if (null == value) removeValue();
        else addValue();
    }

    @Override
    public final T get() {
        T value = super.get();
        if (null != value) addValue();
        return value;
    }
    
    private void removeValue() {
        holder.get().remove(this); //从holder持有的map对象中移除
    }

    private void addValue() {
        if (!holder.get().containsKey(this)) {
            holder.get().put(this, null); //从holder持有的map对象中添加
        }
    }

TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过第一部分的例子,可以发现,除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以,那么,下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):

首先打开包装类,看下execute方法在执行时做了些什么。

// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
    public void execute(@Nonnull Runnable command) {
        executor.execute(TtlRunnable.get(command)); //这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
    }

    // 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
    @Nullable
    public static TtlRunnable get(@Nullable Runnable runnable) {
        return get(runnable, false, false);
    }

    // 对应上面的get方法
    @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); //最终初始化
    }

    // 对应上面的TtlRunnable方法
    private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.capturedRef = new AtomicReference<Object>(capture()); //这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    // 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
    @Nonnull
    public static Object capture() {
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) { // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
            captured.put(threadLocal, threadLocal.copyValue());
        }
        return captured; // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
    }

    // 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
    private T copyValue() {
        return copy(get());
    }
    protected T copy(T parentValue) {
        return parentValue;
    }

结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef属性,到此为止,下面会发生什么,我们大致上可以猜出来了,接下来大概率会在run方法里,将这些捕获到的值赋给子线程的holder赋对应的TTL值,那么我们继续往下看Rannable包装类里的run方法是怎么实现的:

//run方法属于Rannable的包装类TtlRunnable

@Override
    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!");
        }

        /**
         * 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值(原生本地变量,下面会详细讲),
         * backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
         */
        Object backup = replay(captured);
        try {
            runnable.run(); // 执行异步逻辑
        } finally {
            restore(backup); // 结合上面对于replay的解释,不难理解,这个方法就是用来恢复原有值的
        }
    }

根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。

下面来具体看这俩方法:

//下面的方法均属于TTL的静态内部类Transmittable

@Nonnull
    public static Object replay(@Nonnull Object captured) {
        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; //使用此线程异步时捕获到的父线程里的本地变量值
        Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); //当前线程原生的本地变量,用于使用完线程后恢复用

        //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
        for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
             iterator.hasNext(); ) {
            Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
            TransmittableThreadLocal<?> threadLocal = next.getKey();

            backup.put(threadLocal, threadLocal.get()); // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用

            /**
             * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
             * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
             * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
             * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
             * 意味着很可能会影响到任务里取值的准确性。
             *
             * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
             * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
             * 该tl对象取到值2,不符合预期
             */
            if (!capturedMap.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        // 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值)
        setTtlValuesTo(capturedMap);

        // 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述
        doExecuteCallback(true);

        return backup;
    }

    // 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的
    public static void restore(@Nonnull Object backup) {
        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
        // call afterExecute callback
        doExecuteCallback(false);

        // 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量)
        for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
             iterator.hasNext(); ) {
            Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
            TransmittableThreadLocal<?> threadLocal = next.getKey();

            /**
             * 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同?
             * 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值)
             */
            if (!backupMap.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        // 同样调用这个方法,进行值的恢复
        setTtlValuesTo(backupMap);
    }

    // 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法
    private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
        for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
            @SuppressWarnings("unchecked")
            TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
            threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现
        }
    }

ok,到这里基本上把TTL比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:

图1

上图第一行指的是类名称,下面的流程指的是类所做的事情,根据上面罗列出来的源码,结合这个时序图,可以比较直观一些的理解整个流程。

三、TTL中线程池子线程原生变量的产生

这一节是为了验证上面replayrestore,现在通过一个例子来验证下,先把源码down下来,在源码的restorereplay上分别加上输出语句,遍历holder

//replay前后打印holder里面的值
public static Object replay(@Nonnull Object captured) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
            Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();
            System.out.println("--------------------replay前置,当前拿到的holder里的TTL列表");
            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                System.out.println(String.format("replay前置里拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
            }

            for...//代码省略,具体看上面
            
            setTtlValuesTo(capturedMap);

            doExecuteCallback(true);

            System.out.println("--------------------reply后置,当前拿到的holder里的TTL列表");
            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                System.out.println(String.format("replay后置里拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
            }

            return backup;
        }

//restore前后打印holder里面的值
public static void restore(@Nonnull Object backup) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
            // call afterExecute callback
            doExecuteCallback(false);

            System.out.println("--------------------restore前置,当前拿到的holder里的TTL列表");
            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                System.out.println(String.format("restore前置里拿到当前线程内变量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
            }

            for...//省略代码,具体具体看上面

            setTtlValuesTo(backupMap);

            System.out.println("--------------------restore后置,当前拿到的holder里的TTL列表");
            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                System.out.println(String.format("restore后置里拿到当前线程内变量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
            }
        }

代码这样做的目的,就是要说明线程池所谓的原生本地变量是怎么产生的,以及replay和restore是怎么设置和恢复的,下面来看个简单的例子:

private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    private static ThreadLocal tl = new TransmittableThreadLocal();
    private static ThreadLocal tl2 = new TransmittableThreadLocal();

    public static void main(String[] args) throws InterruptedException {

        tl.set(1);
        tl2.set(2);

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

运行结果如下:

--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=1259475182, ttl_value=2
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=1259475182, ttl_value=2
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=1259475182, ttl_value=2
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=1259475182, ttl_value=2
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1

我们会发现,原生值产生了,从异步开始,就确定了线程池里的线程具备了1和2的值,那么,再来改动下上面的测试代码:

public static void main(String[] args) throws InterruptedException {

        tl.set(1);

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread.sleep(1000L);

        tl2.set(2);//较第一次换下位置,换到第一次使用线程池后执行(这意味着下面这次异步不会再触发Thread的init方法了)

        System.out.println("---------------------------------------------------------------------------------");
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

运行结果为:

--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
---------------------------------------------------------------------------------
--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=1020371697, ttl_value=2
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=1020371697, ttl_value=2
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1

可以发现,第一次异步时,只有一个值被传递了下去,然后第二次异步,新加了一个tl2的值,但是看第二次异步的打印,会发现,restore恢复后,仍然是第一次异步发生时放进去的那个tl的值。

通过上面的例子,基本可以确认,所谓线程池内线程的本地原生变量,其实是第一次使用线程时被传递进去的值,我们之前有说过TTL是继承至ITL的,之前的文章也说过,线程池第一次启用时是会触发Threadinit方法的,也就是说,在第一次异步时拿到的主线程的变量会被传递给子线程,作为子线程的原生本地变量保存起来,后续是replay操作和restore操作也是围绕着这个原生变量(即原生holder里的值)来进行设置恢复的,设置的是当前父线程捕获到的本地变量,恢复的是子线程原生本地变量。

holder里持有的可以理解就是当前线程内的所有本地变量,当子线程将异步任务执行完毕后会执行restore进行恢复原生本地变量,具体参照上面的代码和测试代码。

四、总结

到这里基本上确认了TTL是如何进行线程池传值的,以及被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量(目前原生本地变量的产生,就只碰到上述测试代码中的这一种情况,即线程第一次使用时通过ITL属性以及Threadinit方法传给子线程,还不太清楚有没有其他方式设置)。

其实,正常程序里想要完成线程池上下文传递,使用TL就足够了,我们可以效仿TTL包装线程池对象的原理,进行值传递,异步任务结束后,再remove,以此类推来完成线程池值传递,不过这种方式过于单纯,且要求上下文为只读对象,否则子线程存在写操作,就会发生上下文污染。

参考博客:https://exceting.github.io/2019/02/20/ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析/

标签:变量,Thread,get,变量值,编程,并发,线程,TransmittableThreadLocal,名称
From: https://www.cnblogs.com/luojw/p/18413953

相关文章

  • 《 C++ 修炼全景指南:九 》打破编程瓶颈!掌握二叉搜索树的高效实现与技巧
    摘要本文详细探讨了二叉搜索树(BinarySearchTree,BST)的核心概念和技术细节,包括插入、查找、删除、遍历等基本操作,并结合实际代码演示了如何实现这些功能。文章深入分析了二叉搜索树的性能优势及其时间复杂度,同时介绍了前驱、后继的查找方法等高级功能。通过自定义实现的......
  • 一个小例子,给你讲透典型的 Go 并发操作
    如果你有一个任务可以分解成多个子任务进行处理,同时每个子任务没有先后执行顺序的限制,等到全部子任务执行完毕后,再进行下一步处理。这时每个子任务的执行可以并发处理,这种情景下适合使用sync.WaitGroup。虽然sync.WaitGroup使用起来比较简单,但是一不留神很有可能踩到坑里。sync.......
  • Golang并发编程中匿名函数的应用与性能考量
    在Golang并发编程中,匿名函数是一种常见且非常实用的编程工具。匿名函数是指没有名称的函数,可以在函数内部定义和使用,或者作为参数传递给其他函数。这种方式不仅简化了代码结构,还能增强代码的灵活性,尤其在并发编程中,匿名函数的使用更能展现其优势。不过,如何合理使用匿名函数并在实......
  • QT6 QML编程
    QT6QML编程使用AI技术辅助生成QT界面美化视频课程QT性能优化视频课程QT原理与源码分析视频课程QTQMLC++扩展开发视频课程免费QT视频课程您可以看免费1000+个QT技术视频免费QT视频课程QT统计图和QT数据可视化视频免费看免费QT视频课程QT性能优化视频免费看免费QT视......
  • QT6 QML编程
    QT6QML编程使用AI技术辅助生成[QT界面美化视频课程](https://edu.csdn.net/lecturer/7637)[QT性能优化视频课程](https://edu.csdn.net/lecturer/7637)[QT原理与源码分析视频课程](https://edu.csdn.net/lecturer/7637)[QTQMLC++扩展开发视频课程](https://edu.csdn.net/lectu......
  • 高并发环境中保持幂等性
    在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法:唯一标识符:请求唯一标识:在每次请求中引入唯一标识符(如UUID或者生成的唯一ID),在处理请求时,系......
  • 网络编程介绍&TCP&UDP协议
    1.网络编程入门1.1网络编程概述计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统网络编程在网络通信协议下,不同计算机上运行的程序......
  • 新手编程竞赛入门指南 [Cloned]
    欢迎加入编程竞赛协会(QQ群:)进入协会QQ群,联系纳新管理员xixu、ssmy,需提交信息(学院、班级、姓名、学号,vjudge帐号),通过审核后邀请加入“程序设计竞赛组”开展编程学习做题训练。受人数限制,一段时间不参与活动者会被移除。新手编程竞赛入门指南编程是程序设计的简称。计算机通过各种......
  • Unity网络编程(1)线程
    引入:网络编程基础认识1.了解操作系统的分时操作:操作系统将时间划分为很多个片段,尽可能均匀地分配给正在执行的线程获得时间片的进程得以运行,其他则在等待CPU在这些进程上来回切换,频密,让人感觉多个进程在同时执行2.概念认识:(1)进程是程序的边界,程序与程序间以进程为隔离 ......
  • 【HBuilderX-从下载到项目创建】编程初学者适用的HBuilderX开发环境(超详细的)下载安装
    简介:HBuilderX是一款由DCloud公司开发的集成开发环境(IDE),专为前端开发设计,同时也支持多平台应用开发。它支持HTML、CSS、JavaScript、Vue、React、Uni-app等多种编程语言和框架,具备代码编辑、调试、测试等功能,并且提供了丰富的插件生态系统以扩展其功能。“......