Closed
Description
因为TTL
底层使用ITL
,会导致在new
线程的时候,父子线程的数据传递,且无法销毁。
背景:
- 项目启动的时候,存在
TTL
的get
操作,于是main
线程存在TTL
的value
; - 当请求进入时,
Tomcat
线程池(不会被TtlExecutors
装饰)会开启子线程来执行业务逻辑; main
线程会将TTL
(此时仅可看做ITL
)的值传递到子线程;- 子线程修改
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();
}
};
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
oldratlee commentedon Jun 28, 2021
解决
Inheritable
能力/功能 引发的问题其中,
ITL
(InheritableThreadLocal
)引发的问题 在 前一个你的 Issue #281 (comment) 中,说明了问题与解法。对于你的场景是线程池;线程池是业务逻辑无关的,应该disable Inheritable。
更方便、合理的解决方法可以是:
通过设置线程池的
ThreadFactory
成DisableInheritableThreadFactory
,disable线程池的Inheritable。对应你的示例代码,修改如下: @yexuerui
线程安全问题
TTL
提供了 数据的传递的能力(作为ThreadLocal
也持有了数据);而传递的对象的线程安全问题,需要业务逻辑来解决。 @yexuerui
TTL
自身的内部数据有并发问题,则是TTL
的Bug;TTL
的外部数据,可以足够复杂。TTL
控制接管不了(当然TTL
也没有去接管 :")。通用基础的并发问题,既不限于也独立于
TTL
的使用:只要一个对象传递到了不同的线程(不再有线程封闭),就需要关注这个对象的线程安全问题。
线程安全/并发安全 的通用解决思路:
Immutable
)String
。String
是Immutable
的,所以线程安全。HashMap
,但浅拷贝了,保证了Map
这一级的线程安全。Map
里的KV
仍然需要继续设计/实现 以保证线程安全。(重复应用这份通用解决思路)MyXxxContext
类。MyXxxContext
实现成是可以并发的。Map
引用,则可以用ConcurrentHashMap
,以保证Map
这一级的线程安全。Map
里的KV
仍然需要继续设计/实现 以保证线程安全。(重复应用这份通用解决思路)注意:线程安全 不代表 业务逻辑正确。业务逻辑正确 还和你的业务流程设计 相关。
这里不再展开 线程安全 的更多讨论了。
并发安全/线程安全 相关Issue
yexuerui commentedon Jun 28, 2021
您好。我明白可以装饰ThreadFactory来解决ITL的问题。
但是我重点强调的是:
请求进来时,是tomcat开启一个线程处理;
但是tomcat的线程池没有使用ttl的包装的线程池,也就无法使用您说的上面的方法。
oldratlee commentedon Jun 28, 2021
childValue
的方式,也可以做到 线程安全new Thread
时,这个TTL
实例就不会传递了childValue
的方式,关闭了这个TTL
实例的Inheritable
能力。如果只希望在
Tomcat
线程池中关闭Inheritable
,可以的做法是:Tomcat
Tomcat
线程池的ThreadFactory
,Wrap成DisableInheritableThreadFactory
PS:
Tomcat
与TTL
的相关Issueyexuerui commentedon Jun 28, 2021
好的,我理解您的意思了
[-]SpringBoot启动时,调用TTL的get方法造成线程不安全[/-][+]使用方式:SpringBoot启动时,调用TTL的get方法如何保证线程安全[/+]oldratlee commentedon Jun 28, 2021
👍 👏 🎉 @yexuerui
带上 并发/多线程 维度时,要想解释清楚,是比较费时费力的~ 🤣 🤯
之前的Issue,涉及并发多线程时,
我一般简单说明,这些并发使用问题与
TTL
功能是独立正交的,尽量避免展开去解释。 😅
HuangDayu commentedon Mar 3, 2022
@oldratlee 你好,
TTL
作为一个公共静态常量使用,应用全局使用该常量ThreadFactory
设置为了TtlExecutors.getDefaultDisableInheritableThreadFactory()
但是依然存在线程安全问题。
问题表现为,
Spring EventBus
的使用中、高并情况下,TTL
的值,事件消费者(B线程池,Java agent
装饰)消费新事件时,尝试过
-javaagent
装饰线程池的方式,也与手动装饰的方式一同使用,但是依然存在该问题。问题是否跟
static
和final
的线程安全性有关?ConcurrentMap
应该是安全的。由于业务相对复杂,但是我尝试用测试用例复现,却没能复现出来 😭 ,
所以暂时没有示例,不知我表达清楚没有,还望答疑解惑一下,谢谢。
oldratlee commentedon Mar 3, 2022
@HuangDayu 独立的问题,请开个新的 issue。 🙏
上面你列的这些前提,如
TTL
作为一个公共静态常量使用ThreadFactory
设置为了TtlExecutors.getDefaultDisableInheritableThreadFactory()
-javaagent
装饰线程池的方式 or 手动装饰的方式ConcurrentMap
是安全的并不能保证 在你业务中 取得 你期望的新值或旧值。
『并不能保证』的一个简单举例 就是
TTL
值。如果不能排除『一段在你业务之中你意料之外的逻辑 改写了
TTL
值』,因为论证逻辑不完整,不能得到『会是什么值』的相关结论。
@HuangDayu 你可以优先找一下 有没有这样意料之外的改写逻辑。即使在业务代码复杂后找起来不容易。
基础件(如
TTL
、ConcurrentMap
)出问题的概率很小(因为被大量使用与验证)。当然确保你正确地理解与使用了这些基础件。
一个复现Demo,因为有全部的运行逻辑代码,可以用于排除或证实
TTL
值』。PS:
能方便确定 没有『业务之中意料之外的逻辑』,
是 良好系统设计的目标与体现。比如 做好封装。
HuangDayu commentedon Mar 4, 2022
@oldratlee 你好,我已参照TtlMDCAdapter 解决了该问题,非常感谢你的解答,谢谢。