alibaba / transmittable-thread-local

📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.
https://github.com/alibaba/transmittable-thread-local
Apache License 2.0
7.59k stars 1.69k forks source link

agent中对普通Thread场景进行包装,来触发copy方法 #517

Closed songxiaosheng closed 1 year ago

songxiaosheng commented 1 year ago

描述

使用TransmittableThreadLocal时候对普通的Thread会实现自动拷贝线程数据,如果是ThreadLocal中的数据是对象只能进行深拷贝,无法触发重写的copy方法,如果是否agent中可以增加对普通线程的处理为Runnable增加包装让普通线程传递数据时候也触发copy方法

期望

期望普通线程的传递也能触发重写的copy方法,这样可以统一管控对象传递过程中的数据内容

样例代码

测试代码如下:

public class ObjectCopyInThreadDemo {

    private final TransmittableThreadLocal<TtlObject> ttlObjectThreadLocal = new TransmittableThreadLocal<TtlObject>() {
        @Override
        public TtlObject copy(TtlObject parentValue) {
            if(parentValue != null){
                try {
                    return (TtlObject) parentValue.clone();
                } catch (CloneNotSupportedException e) {
                    throw new RuntimeException(e);
                }
            }
            return super.copy(parentValue);
        }
    };
    public static void main(String[] args) {
        ThreadDemo a = new ThreadDemo();
        a.doBussiness();
    }
    public void doBussiness() {

        TtlObject value = new TtlObject();
        System.out.println(value);
        ttlObjectThreadLocal.set(value);

// 未使用TtlRunnable类型时候不会走重写的copy方法,默认情况是会共享对象
//        (new Thread(TtlRunnable.get(new Runnable() {
        (new Thread((new Runnable() {

            @Override
            public void run() {
                System.out.println("子线程启动");
                TtlObject ttlObject = ttlObjectThreadLocal.get();
                System.out.println(ttlObject);
            }
        }))).start();
    }
}
public class TtlObject {

    private String name;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        TtlObject ttlObject = new TtlObject();
        ttlObject.setName(this.name);
        return ttlObject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
oldratlee commented 1 year ago

具体你的需求,对应的用法 是 重写(overrideInheritableThreadLocal.childValue()以代理到copy()方法: @songxiaosheng 💕

protected TtlObject childValue​(TtlObject parentValue) {
  return copy(parentValue);
}

PS: Agent使用方式 与 copy方法 是独立正交的。

Agent方式只是在使用上的便利,省去API方式要写的代码行(如调用TtlRunnable.get()TtlExecutors.getTtlExecutorService())。


更多这方面的使用与设计及其原因的说明 在已有的 issue 中有展开讨论: @songxiaosheng

摘一些内容如下:

关于初始化方法

相关的初始化方法还有 [ThreadLocal#initialValue()](https://docs.oracle.com/javase/10/docs/api/java/lang/ThreadLocal.html#initialValue())。

这3个初始化方法 的区别是 做初始化操作的 生命周期时间点/触发时间 不同:

  1. ThreadLocal#initialValue()
    • ThreadLocal没有值时,取值(ThreadLocal#get()方法)触发初始化。
    • 下面是一些注意点
    • ThreadLocal#initialValue()Lazy的; 即创建ThreadLocal实例时,并不会触发ThreadLocal#initialValue()调用。
    • 先设置值(ThreadLocal#set(T))再取值,则不会触发调用ThreadLocal#initialValue();因为已经有值了。 即使设置的是null,也不会触发。
    • 调用ThreadLocal#remove()再取值,会触发调用ThreadLocal#initialValue();因为没有值了。
  2. InheritableThreadLocal#childValue(T)
    • 创建新线程时,用于初始化子线程的InheritableThreadLocal值。
  3. TransmittableThreadLocal#copy(T)
    • 传递时,用于初始化 在任务(如TtlRunnable)执行中的TransmittableThreadLocal值。

不同初始化方法是独立正交的

这里可能值得强调一下这3个初始化方法的容易被忽视的地方:3个方法是独立正交的。 (好的系统设计应该这样,不同功能独立正交;系统大的功能 是通过 方便地组合/使用更小的功能 来实现。)

展开一些的说明如下:

  • 因为生命周期时间点/触发时间 不同,各自独立发挥作用。
  • 子类包含进来的父类初始化方法,功能不变。
    • InheritableThreadLocalThreadLocal的子类,包含的ThreadLocal#initialValue()功能不变。
    • TransmittableThreadLocalInheritableThreadLocal的子类,包含的ThreadLocal#initialValue()InheritableThreadLocal#childValue(T)功能不变。

系统设计最佳实践:结合业务扩展重写初始化方法,而不是用无业务含义的缺省实现

ThreadLocal的使用中,看到初始化方法的扩展重写没有得到应有的重视与理解使用。

即初始化方法没有重写,结果是在用无业务含义的缺省实现:

  • ThreadLocal#initialValue():返回null
  • InheritableThreadLocal#childValue(T):直接返回入参
  • TransmittableThreadLocal#copy(T):直接返回入参

这样的做法,导致 上面提到的『通过ThreadLocal#set(T)方法来完成初始化工作』。结果会导致:

  • 需要用户主动调用ThreadLocal#set(T)来,用户使用麻烦/困难。
  • 通过用户调用ThreadLocal#set(T)来做初始化操作,因为语义不对应(即系统设计错误), 这样的实现方式往往会引发隐晦偶现的业务逻辑Bug

写一个示意Demo可以这样;但作为实际系统实现,

强烈推荐:结合业务与不同初始化方法的生命周期时间点 扩展重写好 相应的初始化方法。

用于重写初始化方法的TransmittableThreadLocal便利工具方法

TransmittableThreadLocal提供了下面3个静态工具方法withInitial*,可以方便地重写这些初始化方法的实现逻辑: (如果没有静态工具方法withInitial*,就要写一个TransmittableThreadLocal子类来重写初始化方法,相对不方便)

songxiaosheng commented 1 year ago

感谢回复🌺,我研究下