Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
yexuerui opened this issue Jun 28, 2021 · 8 comments
Assignees
Labels
📐 design discussion ❓question Further information is requested

Comments

@yexuerui
Copy link

yexuerui commented Jun 28, 2021

因为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
Copy link
Member

oldratlee commented Jun 28, 2021

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

背景:

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

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

其中,ITLInheritableThreadLocal)引发的问题 在 前一个你的 Issue #281 (comment) 中,说明了问题与解法。

对于你的场景是线程池;线程池是业务逻辑无关的,应该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自身的内部数据有并发问题,则是TTL的Bug;
  • 传递对象是TTL外部数据,可以足够复杂。
    • 无论是传递对象所实现的复杂数据结构,还是其复杂使用方式。
    • 这些复杂性TTL控制接管不了(当然TTL也没有去接管 :")。

通用基础的并发问题,既不限于也独立于 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
Copy link
Author

yexuerui commented Jun 28, 2021

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

但是我重点强调的是:

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

@oldratlee
Copy link
Member

oldratlee commented Jun 28, 2021

我重点强调的是:

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

  • 用覆盖childValue的方式,也可以做到 线程安全
    • 参见上条回复『线程安全问题』部分的讨论内容。
  • new Thread时,这个TTL实例就不会传递了
    • 因为用覆盖childValue的方式,关闭了这个TTL实例的Inheritable能力。

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

  • 看看如何改Tomcat
  • 或 改设置Tomcat线程池的ThreadFactory,Wrap成DisableInheritableThreadFactory

PS: TomcatTTL的相关Issue

@yexuerui
Copy link
Author

好的,我理解您的意思了

@oldratlee oldratlee changed the title SpringBoot启动时,调用TTL的get方法造成线程不安全 使用方式:SpringBoot启动时,调用TTL的get方法如何保证线程安全 Jun 28, 2021
@oldratlee oldratlee self-assigned this Jun 28, 2021
@oldratlee oldratlee added the ❓question Further information is requested label Jun 28, 2021
@oldratlee
Copy link
Member

oldratlee commented Jun 28, 2021

好的,我理解您的意思了

👍 👏 🎉 @yexuerui


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

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

@HuangDayu
Copy link

HuangDayu commented Mar 3, 2022

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

@oldratlee 你好,

  • 我将TTL作为一个公共静态常量使用,应用全局使用该常量
  • 然后也将线程池的ThreadFactory设置为了TtlExecutors.getDefaultDisableInheritableThreadFactory()

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

问题表现为,

  • Spring EventBus的使用中、高并情况下,
  • 事件发布者(A线程池,手动装饰)设置TTL的值,事件消费者(B线程池,Java agent装饰)消费新事件时,
  • 常出现TTL获取到了旧的事件的TTL值,事件并发越高,问题越严重,如果不是高并发则不出现该问题。

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

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

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

@oldratlee
Copy link
Member

oldratlee commented Mar 3, 2022

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


上面你列的这些前提,如

  • TTL作为一个公共静态常量使用
  • 将线程池的ThreadFactory设置为了TtlExecutors.getDefaultDisableInheritableThreadFactory()
  • -javaagent装饰线程池的方式 or 手动装饰的方式
  • ConcurrentMap 是安全的
  • ...

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

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

  • 一段 在你业务之中你意料之外的逻辑 改写了 TTL值。

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


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

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

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

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

  • 『一段 在你业务之中你意料之外的逻辑 改写了 TTL值』。
  • 正确地理解与使用一个基础件。

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

@HuangDayu
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📐 design discussion ❓question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants