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

使用方式:SpringBoot启动时,调用TTL的get方法如何保证线程安全 #282

Closed yexuerui closed 3 years ago

yexuerui commented 3 years ago

因为TTL底层使用ITL,会导致在new线程的时候,父子线程的数据传递,且无法销毁。

背景:

  1. 项目启动的时候,存在TTLget操作,于是main线程存在TTLvalue
  2. 当请求进入时,Tomcat线程池(不会被TtlExecutors装饰)会开启子线程来执行业务逻辑;
  3. main线程会将TTL(此时仅可看做ITL)的值传递到子线程;
  4. 子线程修改TTL的引用时,会造成内存不安全;

代码如下:

@Slf4j
@RestController
public class ThreadLocalController {

    ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }
    };

    /**
     * 项目启动的时候,会调用TTL的get方法,这里使用static模拟;
     */
    static {
        l2.get();
        log.info("项目启动时加载配置");
    }

    @RequestMapping("/local/t1")
    public void t1() throws InterruptedException {
        Map<String, String> mc = l2.get();
        mc.put("t1", "t1v");
        log.info("【/local/t1】主线程打印:" + l2.get());
        executorService.execute(() -> {
            log.info("【/local/t1】子线程2map{}", l2.get());
        });
        Thread.sleep(1000);
        l2.remove();
    }

    @RequestMapping("/local/t4")
    public void t4() {
        log.info("【/local/t4】主线程打印:" + l2.get());
        executorService.execute(() -> {
            log.info("【/local/t4】子线程打印数据{}", l2.get());
        });
        Map<String, String> cache = l2.get();
        cache.put("l4", "l4v");
        l2.remove();
    }

}

疑问:此时由于是普通的线程池,即使TTL重写copy方法也会造成线程不安全;

解决方法只有去重写childValue方法,来解决ITL传递到子线程吗?:

    public static TransmittableThreadLocal<Map<String, String>> l2 = new TransmittableThreadLocal<>() {
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }

        @Override
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            return initialValue();
        }
    };
oldratlee commented 3 years ago

因为TTL底层使用ITL,会导致在new线程的时候,父子线程的数据传递,且无法销毁。

背景:

  1. 项目启动的时候,存在TTLget操作,于是main线程存在TTLvalue
  2. 当请求进入时,Tomcat线程池(不会被TtlExecutors装饰)会开启子线程来执行业务逻辑;
  3. main线程会将TTL(此时仅可看做ITL)的值传递到子线程;
  4. 子线程修改TTL的引用时,会造成内存不安全;

解决Inheritable能力/功能 引发的问题

其中,ITLInheritableThreadLocal)引发的问题 在 前一个你的 Issue https://github.com/alibaba/transmittable-thread-local/issues/281#issuecomment-869000689 中,说明了问题与解法。

对于你的场景是线程池;线程池是业务逻辑无关的,应该disable Inheritable。

更方便、合理的解决方法可以是: 通过设置线程池的ThreadFactoryDisableInheritableThreadFactory,disable线程池的Inheritable。

对应你的示例代码,修改如下: @yexuerui

public class ThreadLocalController {
    ExecutorService executorService = TtlExecutors.getTtlExecutorService(
        new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(),
            // *disable Inheritable*
            // 通过设置线程池的ThreadFactory成DisableInheritableThreadFactory
            TtlExecutors.getDefaultDisableInheritableThreadFactory()
        )
    );

    ......
}

线程安全问题

疑问:此时由于是普通的线程池,即使TTL重写copy方法也会造成线程不安全;

TTL提供了 数据的传递的能力(作为ThreadLocal也持有了数据); 而传递的对象的线程安全问题,需要业务逻辑来解决。 @yexuerui

通用基础的并发问题,既不限于也独立于 TTL的使用:

只要一个对象传递到了不同的线程(不再有线程封闭),就需要关注这个对象的线程安全问题。

JDKInheritableThreadLocal类,业务使用方也一样有线程安全问题 需要注意: InheritableThreadLocal.childValue方法 也 可以实现成 将一个对象引用传递到另一个线程,需要注意&解决线程安全问题。

文档 User Guide

image


线程安全/并发安全 的通用解决思路:

  1. 传递对象 是 不可变的(Immutable
    • 例如,传递的对象是String
    • StringImmutable的,所以线程安全。
  2. 传递对象 是 不共享的,即保证线程封闭
    • 例如,拷贝出一个新的对象来传递,以不共享。
    • 对于上面你的示例代码中,传递的对象是HashMap,但拷贝了,保证了Map这一级的线程安全。
      • Map里的KV 仍然需要继续设计/实现 以保证线程安全。(重复应用这份通用解决思路)
  3. 传递对象 是 并发安全的(如支持并发访问)
    • 例如,如果传递的对象类型是MyXxxContext类。
      • MyXxxContext实现成是可以并发的。
    • 如果传递Map引用,则可以用ConcurrentHashMap,以保证Map这一级的线程安全。
      • Map里的KV 仍然需要继续设计/实现 以保证线程安全。(重复应用这份通用解决思路)

注意:线程安全 不代表 业务逻辑正确。业务逻辑正确 还和你的业务流程设计 相关。

这里不再展开 线程安全 的更多讨论了。

并发安全/线程安全 相关Issue

yexuerui commented 3 years ago

您好。我明白可以装饰ThreadFactory来解决ITL的问题。

但是我重点强调的是:

请求进来时,是tomcat开启一个线程处理; 但是tomcat的线程池没有使用ttl的包装的线程池,也就无法使用您说的上面的方法。

oldratlee commented 3 years ago

我重点强调的是:

请求进来时,是tomcat开启一个线程处理; 但是tomcat的线程池没有使用ttl的包装的线程池,也就无法使用您说的上面的方法。


如果只希望在Tomcat线程池中关闭Inheritable,可以的做法是:

PS: TomcatTTL的相关Issue

yexuerui commented 3 years ago

好的,我理解您的意思了

oldratlee commented 3 years ago

好的,我理解您的意思了

👍 👏 🎉 @yexuerui


带上 并发/多线程 维度时,要想解释清楚,是比较费时费力的~ 🤣 🤯

之前的Issue,涉及并发多线程时, 我一般简单说明,这些并发使用问题与TTL功能是独立正交的, 尽量避免展开去解释。 😅

HuangDayu commented 2 years ago
public static final TransmittableThreadLocal<ConcurrentMap<String, Object>> THREAD_CONTEXT = TransmittableThreadLocal.withInitial(() -> {
    return  new ConcurrentHashMap<>();
});

@oldratlee 你好,

但是依然存在线程安全问题。

问题表现为,

尝试过-javaagent装饰线程池的方式,也与手动装饰的方式一同使用,但是依然存在该问题。

问题是否跟staticfinal的线程安全性有关?ConcurrentMap应该是安全的。

由于业务相对复杂,但是我尝试用测试用例复现,却没能复现出来 😭 , 所以暂时没有示例,不知我表达清楚没有,还望答疑解惑一下,谢谢。

oldratlee commented 2 years ago

@HuangDayu 独立的问题,请开个新的 issue。 🙏


上面你列的这些前提,如

并不能保证 在你业务中 取得 你期望的新值或旧值。

『并不能保证』的一个简单举例 就是

如果不能排除『一段在你业务之中你意料之外的逻辑 改写了 TTL值』, 因为论证逻辑不完整,不能得到『会是什么值』的相关结论。


@HuangDayu 你可以优先找一下 有没有这样意料之外的改写逻辑。即使在业务代码复杂后找起来不容易。

基础件(如TTLConcurrentMap)出问题的概率很小(因为被大量使用与验证)。

当然确保你正确地理解与使用了这些基础件。

一个复现Demo,因为有全部的运行逻辑代码,可以用于排除或证实


PS: 能方便确定 没有『业务之中意料之外的逻辑』, 是 良好系统设计的目标与体现。比如 做好封装。

HuangDayu commented 2 years ago

@oldratlee 你好,我已参照TtlMDCAdapter 解决了该问题,非常感谢你的解答,谢谢。