多线程传递Context multi-thread context(MTC)
来源:互联网 发布:淘宝流量统计工具 编辑:程序博客网 时间:2024/06/03 22:58
在使用线程池等会缓存线程的组件情况下,完成多线程的Context传递。JDK的java.lang.InheritableThreadLocal类可以完成父子线程的Context传递。但对于使用线程池等会缓存线程的组件的情况,线程由线程池创建好,并且线程是缓存起来反复使用的。这时父子线程关系的上下文传递已经没有意义,应用中要做上下文传递,实际上是在把 任务提交给线程池时的上下文传递到 任务执行时。
需求场景 应用容器或上层框架跨应用代码给下层SDK传递信息
举个场景,App Engine(PAAS)上会运行由应用提供商提供的应用(SAAS模式)。多个SAAS用户购买并使用这个应用(即SAAS应用)。SAAS应用往往是一个实例为多个SAAS用户提供服务。
另一种模式是:SAAS用户使用完全独立一个SAAS应用,包含独立应用实例及其后的数据源(如DB、缓存,etc)。
需要避免的SAAS应用拿到多个SAAS用户的数据。
一个解决方法是处理过程关联一个SAAS用户的上下文,在上下文中应用只能处理(读&写)这个SAAS用户的数据。
请求由SAAS用户发起(如从Web请求进入App Engine),App Engine可以知道是从哪个SAAS用户,在Web请求时在上下文中设置好SAAS用户ID。
应用处理数据(DB、Web、消息 etc.)是通过App Engine提供的服务SDK来完成。当应用处理数据时,SDK检查数据所属的SAAS用户是否和上下文中的SAAS用户ID一致,如果不一致则拒绝数据的读写。
应用代码会使用线程池,并且这样的使用是正常的业务需求。SAAS用户ID的从要App Engine传递到下层SDK,要支持这样的用法。
日志记录系统上下文
App Engine的日志(如,SDK会记录日志)要记录系统上下文。由于不限制用户应用使用线程池,系统的上下文需要能跨线程的传递,且不影响应用代码。
上面场景使用MTC的整体构架 :
构架涉及3个角色:容器、用户应用、SDK。
整体流程:
1、请求进入PAAS容器,提取上下文信息并设置好上下文。
2、进入用户应用处理业务,业务调用SDK(如DB、消息、etc)。
3、用户应用会使用线程池,所以调用SDK的线程可能不是请求的线程。
4、进入SDK处理。
5、提取上下文的信息,决定是否符合拒绝处理。
整个过程中,上下文的传递 对于 用户应用代码 期望是透明的。
User Guide
使用类MtContextThreadLocal来保存上下文,并跨线程池传递。
MtContextThreadLocal继承java.lang.InheritableThreadLocal,使用方式也类似。
比java.lang.InheritableThreadLocal,添加了protected方法copy,用于定制 任务提交给线程池时的上下文传递到 任务执行时时的拷贝行为,缺省是传递的是引用。
具体使用方式见下面的说明。
1. 简单使用
父线程给子线程传递Context。
示例代码:
// 在父线程中设置 MtContextThreadLocal<String> parent = new MtContextThreadLocal<String>(); parent.set("value-set-in-parent"); // 在子线程中可以读取, 值是"value-set-in-parent" String value = parent.get();
这是其实是java.lang.InheritableThreadLocal的功能,应该使用java.lang.InheritableThreadLocal来完成。
但对于使用了异步执行(往往使用线程池完成)的情况,线程由线程池创建好,并且线程是缓存起来反复使用的。
这时父子线程关系的上下文传递已经没有意义,应用中要做上下文传递,实际上是在把 任务提交给线程池时的上下文传递到 任务执行时。 解决方法参见下面的这几种用法。
2. 保证线程池中传递Context 2.1 修饰Runnable和Callable
使用com.alibaba.mtc.MtContextRunnable和com.alibaba.mtc.MtContextCallable来修饰传入线程池的Runnable和Callable。
示例代码:
MtContextThreadLocal<String> parent = new MtContextThreadLocal<String>(); parent.set("value-set-in-parent"); Runnable task = new Task("1"); // 额外的处理,生成修饰了的对象mtContextRunnable Runnable mtContextRunnable = MtContextRunnable.get(task); executorService.submit(mtContextRunnable); // Task中可以读取, 值是"value-set-in-parent" String value = parent.get();//上面演示了Runnable,Callable的处理类似MtContextThreadLocal<String> parent = new MtContextThreadLocal<String>(); parent.set("value-set-in-parent");Callable call = new Call("1"); // 额外的处理,生成修饰了的对象mtContextCallable Callable mtContextCallable = MtContextCallable.get(call); executorService.submit(mtContextCallable); // Call中可以读取, 值是"value-set-in-parent" String value = parent.get();
这种使用方式的时序图 :
2.2 修饰线程池
省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成。
通过工具类com.alibaba.mtc.threadpool.MtContextExecutors完成,有下面的方法:
getMtcExecutor:修饰接口ExecutorgetMtcExecutorService:修饰接口ExecutorServiceScheduledExecutorService:修饰接口ScheduledExecutorService
示例代码:
ExecutorService executorService = ... // 额外的处理,生成修饰了的对象executorService executorService = MtContextExecutors.getMtcExecutorService(executorService); MtContextThreadLocal<String> parent = new MtContextThreadLocal<String>(); parent.set("value-set-in-parent"); Runnable task = new Task("1"); Callable call = new Call("2"); executorService.submit(task); executorService.submit(call); // Task或是Call中可以读取, 值是"value-set-in-parent" String value = parent.get();
2.3 使用Java Agent来修饰JDK线程池实现类
这种方式,实现线程池的MtContext传递过程中,代码中没有修饰Runnble或是线程池的代码。
即可以做到应用代码 无侵入,后面文档有结合实际场景的架构对这一点的说明。
示例代码:
// 框架代码 MtContextThreadLocal<String> parent = new MtContextThreadLocal<String>(); parent.set("value-set-in-parent"); // 应用代码 ExecutorService executorService = Executors.newFixedThreadPool(3); Runnable task = new Task("1"); Callable call = new Call("2"); executorService.submit(task); executorService.submit(call); // Task或是Call中可以读取, 值是"value-set-in-parent" String value = parent.get();
Demo参见AgentDemo.java。
目前Agent中,修饰了jdk中的两个线程池实现类(实现代码在MtContextTransformer.java):
java.util.concurrent.ThreadPoolExecutorjava.util.concurrent.ScheduledThreadPoolExecutor
在Java的启动参数加上:
-Xbootclasspath/a:/path/to/multithread.context-1.1.0.jar-javaagent:/path/to/multithread.context-1.1.0.jar
注意:
Agent修改是JDK的类,类中加入了引用MTC的代码,所以MTC Agent的Jar要加到bootclasspath上。
Java命令行示例如下:
java -Xbootclasspath/a:multithread.context-1.1.0.jar \ -javaagent:multithread.context-1.1.0-SNAPSHOT.jar \ -cp classes \ com.alibaba.mtc.threadpool.agent.demo.AgentDemo
有Demo演示『使用Java Agent来修饰线程池实现类』,执行工程下的脚本run-agent-demo.sh即可运行Demo。
什么情况下,Java Agent的使用方式MtContext会失效?
由于Runnable和Callable的修饰代码,是在线程池类中插入的。下面的情况会让插入的代码被绕过,MtContext会失效。
用户代码中继承java.util.concurrent.ThreadPoolExecutor和java.util.concurrent.ScheduledThreadPoolExecutor, 覆盖了execute、submit、schedule等提交任务的方法,并且没有调用父类的方法。修改线程池类的实现,execute、submit、schedule等提交任务的方法禁止这些被覆盖,可以规避这个问题。目前,没有修饰java.util.Timer类,使用Timer时,MtContext会有问题。
Developer Guide Java Agent方式对应用代码无侵入
相对修饰Runnble或是线程池的方式,Java Agent方式为什么是应用代码无侵入的?
按框架图,把前面示例代码操作可以分成下面几部分:
读取信息设置到MtContext。
这部分在容器中完成,无需应用参与。
提交Runnable到线程池。要有修饰操作Runnable(无论是直接修饰Runnble还是修饰线程池)。
这部分操作一定是在用户应用中触发。
读取MtContext,做业务检查。
在SDK中完成,无需应用参与。
只有第2部分的操作和应用代码相关。
如果不通过Java Agent修饰线程池,则修饰操作需要应用代码来完成。
使用Java Agent方式,应用无需修改代码,即做到 相对应用代码 透明地完成跨线程池的上下文传递。
如何权衡Java Agent方式的失效情况
把这些失效情况都解决了是最好的,但复杂化了实现。下面是一些权衡:
不推荐使用Timer类,推荐用ScheduledThreadPoolExecutor。 ScheduledThreadPoolExecutor实现更强壮,并且功能更丰富。 如支持配置线程池的大小(Timer只有一个线程);Timer在Runnable中抛出异常会中止定时执行。覆盖了execute、submit、schedule的问题的权衡是: 业务上没有修改这些方法的需求。并且线程池类提供了beforeExecute方法用于插入扩展的逻辑。
已有Java Agent中嵌入MtContext Agent
这样可以减少Java命令上Agent的配置。
在自己的ClassFileTransformer中调用MtContextTransformer,
示例代码如下:
public class TransformerAdaptor implements ClassFileTransformer { final MtContextTransformer mtContextTransformer = new MtContextTransformer(); final byte[] transform = mtContextTransformer.transform( loader, className, classBeingRedefined, protectionDomain, classfileBuffer); if (transform != null) { return transform; } // Your transform code ... return null; } }
注意还是要在bootclasspath上,加上MtContext依赖的2个Jar:
-Xbootclasspath/a:/path/to/multithread.context-1.1.0.jar:/path/to/your/agent/jar/files
Bootstrap上添加通用库的Jar的问题及解决方法
通过Java命令参数-Xbootclasspath把库的Jar加Bootstrap ClassPath上。Bootstrap ClassPath上的Jar中类会优先于应用ClassPath的Jar被加载,并且不能被覆盖。
MTC在Bootstrap ClassPath上添加了Javassist的依赖,如果应用中如果使用了Javassist,实际上会优先使用Bootstrap ClassPath上的Javassist,即应用不能选择Javassist的版本,应用需要的Javassist和MTC的Javassist有兼容性的风险。
可以通过repackage(重新命名包名)来解决这个问题。
Maven提供了Shade插件,可以完成repackage操作,并把Javassist的类加到MTC的Jar中。
这样就不需要依赖外部的Javassist依赖,也规避了依赖冲突的问题。
Java API Docs
当前版本的Java API文档地址: http://alibaba.github.io/multi-thread-context/apidocs/
Maven依赖
示例:
<dependency> <groupId>com.alibaba</groupId> <artifactId>multithread.context</artifactId> <version>1.1.0</version> </dependency>
可以在 search.maven.org 查看可用的版本。
性能测试 内存泄漏
对比测试MtContextThreadLocal和ThreadLocal,测试Case是:
简单一个线程一直循环new MtContextThreadLocal、ThreadLocal实例,不主动做任何清理操作,即不调用ThreadLocal的remove方法主动清空。
验证结果
都可以持续运行,不会出内存溢出OutOfMemoryError。
执行方式
可以通过执行工程下的脚本来运行Case验证:
脚本run-memoryleak-ThreadLocal.sh运行ThreadLocal的测试。测试类是NoMemoryLeak_ThreadLocal_NoRemove。脚本run-memoryleak-MtContextThreadLocal.sh运行MtContextThreadLocal的测试。 测试类是NoMemoryLeak_MtContextThreadLocal_NoRemove。
TPS & 压力测试
对比测试MtContextThreadLocal和ThreadLocal,测试Case是:
2个线程并发一直循环new MtContextThreadLocal、ThreadLocal实例,不主动做任何清理操作,即不调用ThreadLocal的remove方法主动清空。
验证结果
在我的4核开发机上运行了24小时,稳定正常。
TPS结果如下:
ThreadLocal的TPS稳定在~41K:
……
tps: 42470
tps: 40940
tps: 41041
tps: 40408
tps: 40610
MtContextThreadLocal的TPS稳定在~40K:
……
tps: 40461
tps: 40101
tps: 39989
tps: 40684
tps: 41174
GC情况如下(1分钟输出一次):
ThreadLocal的每分钟GC时间是5.45s,FGC次数是0.09:
MtContextThreadLocal的每分钟GC时间是5.29s,FGC次数是3.27:
TPS略有下降的原因分析
使用jvisualvm Profile方法耗时,MtContextThreadLocalCase的热点方法和ThreadLocalCase一样。
略有下降可以认为是Full GC更多引起。
实际使用场景中,MtContextThreadLocal实例个数非常有限,不会有性能问题。
FGC次数增多的原因分析
在MtContextThreadLocal.holder中,持有MtContextThreadLocal实例的弱引用,减慢实例的回收,导致Full GC增加。
实际使用场景中,MtContextThreadLocal实例个数非常有限,不会有性能问题。
执行方式
可以通过执行工程下的脚本来运行Case验证:
脚本run-tps-ThreadLocal.sh运行ThreadLocal的测试。测试类是CreateThreadLocalInstanceTps。run-tps-MtContextThreadLocal.sh运行MtContextThreadLocal的测试。 测试类是CreateMtContextThreadLocalInstanceTps。
FAQ
Mac OS X下,使用javaagent,可能会报JavaLaunchHelper的出错信息。
JDK Bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=8021205
可以换一个版本的JDK。我的开发机上1.7.0_40有这个问题,1.6.0_51、1.7.0_45可以运行。
1.7.0_45还是有JavaLaunchHelper的出错信息,但不影响运行。相关资料 Jdk core classes
WeakHashMapInheritableThreadLocal
Java Agent
Java Agent规范Java SE 6 新特性: Instrumentation 新功能Creation, dynamic loading and instrumentation with javaagentsJavaAgent加载机制分析
Javassist
Getting Started with Javassist
Shade插件
Maven的Shade插件
- 多线程传递Context multi-thread context(MTC)
- Multi-Context CoreData
- Context 传递数据(转载)
- Context 传递数据
- 传递 context 导致 NullPointerException
- React数据传递---context
- Context
- Context
- Context
- context
- Context
- context
- Context
- Context
- Context
- Context
- Context
- Context
- WebService之基于REST机制的实现实例(Java版)
- php动态扩展函数库依赖
- 问题 Q: 铺地毯
- [生存志] 第145节 班固著汉书
- 性能工具造数据 (jmeter)
- 多线程传递Context multi-thread context(MTC)
- 闫士佳的第一篇博客——subgrid
- Androidstudio快捷键(转载请注明:Android开发中文站 » Android Studio 快捷键)
- 【斯坦福大学-机器学习】2.单变量线性回归(一)
- VMware fusion下ubuntu(16)虚拟机编译安卓源码笔记
- 2017年2月员工工资结算
- 线段树入门
- Windows环境下使用强大的wget工具
- 我是优雅的分割线————————————————————————————————————————————