在使用线程池等会缓存线程的组件情况下,完成多线程的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:修饰接口Executor
getMtcExecutorService:修饰接口ExecutorService
ScheduledExecutorService:修饰接口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 parent = new MtContextThreadLocal();
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.ThreadPoolExecutor
java.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.alibabagroupId> <artifactId>multithread.contextartifactId> <version>1.1.0version>
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:
S0 | S1 | E | O | P | YGC | YGCT | FGC | FGCT | GCT |
---|---|---|---|---|---|---|---|---|---|
0.00 | 97.66 | 0.00 | 8.33 | 12.70 | 1470935 | 2636.215 | 41 | 0.229 | 2636.444 |
97.66 | 0.00 | 0.00 | 17.18 | 12.70 | 1473968 | 2640.597 | 41 | 0.229 | 2640.825 |
98.44 | 0.00 | 0.00 | 25.47 | 12.70 | 1477020 | 2645.265 | 41 | 0.229 | 2645.493 |
96.88 | 0.00 | 33.04 | 34.03 | 12.70 | 1480068 | 2650.149 | 41 | 0.229 | 2650.378 |
0.00 | 97.66 | 14.01 | 41.82 | 12.70 | 1483113 | 2655.262 | 41 | 0.229 | 2655.490 |
0.00 | 97.66 | 74.07 | 50.25 | 12.70 | 1486149 | 2660.596 | 41 | 0.229 | 2660.825 |
96.88 | 0.00 | 0.00 | 58.32 | 12.70 | 1489170 | 2666.135 | 41 | 0.229 | 2666.364 |
98.44 | 0.00 | 26.07 | 67.05 | 12.70 | 1492162 | 2671.841 | 41 | 0.229 | 2672.070 |
0.00 | 97.66 | 0.00 | 76.50 | 12.70 | 1495139 | 2677.809 | 41 | 0.229 | 2678.038 |
0.00 | 97.66 | 0.00 | 85.95 | 12.70 | 1498091 | 2683.994 | 41 | 0.229 | 2684.222 |
96.88 | 0.00 | 0.00 | 96.50 | 12.70 | 1501038 | 2690.454 | 41 | 0.229 | 2690.683 |
97.66 | 0.00 | 0.00 | 7.96 | 12.70 | 1504054 | 2695.583 | 42 | 0.233 | 2695.816 |
0.00 | 97.66 | 0.00 | 17.46 | 12.70 | 1507099 | 2700.009 | 42 | 0.233 | 2700.241 |
0.00 | 97.66 | 0.00 | 26.97 | 12.70 | 1510133 | 2704.652 | 42 | 0.233 | 2704.885 |
97.66 | 0.00 | 0.00 | 36.57 | 12.70 | 1513158 | 2709.592 | 42 | 0.233 | 2709.825 |
0.00 | 97.66 | 0.00 | 45.59 | 12.70 | 1516167 | 2714.738 | 42 | 0.233 | 2714.971 |
98.44 | 0.00 | 0.00 | 54.49 | 12.70 | 1519166 | 2720.109 | 42 | 0.233 | 2720.342 |
0.00 | 98.44 | 0.00 | 63.52 | 12.70 | 1522141 | 2725.688 | 42 | 0.233 | 2725.921 |
0.00 | 97.66 | 84.18 | 72.00 | 12.70 | 1525139 | 2731.579 | 42 | 0.233 | 2731.812 |
0.00 | 98.44 | 20.04 | 80.10 | 12.70 | 1528121 | 2737.680 | 42 | 0.233 | 2737.913 |
0.00 | 97.66 | 28.06 | 87.70 | 12.70 | 1531093 | 2743.991 | 42 | 0.233 | 2744.224 |
0.00 | 98.44 | 0.00 | 95.63 | 12.70 | 1534055 | 2750.508 | 42 | 0.233 | 2750.741 |
97.66 | 0.00 | 0.00 | 4.75 | 12.70 | 1537062 | 2756.196 | 43 | 0.239 | 2756.435 |
MtContextThreadLocal的每分钟GC时间是5.29s,FGC次数是3.27:
S0 | S1 | E | O | P | YGC | YGCT | FGC | FGCT | GCT |
---|---|---|---|---|---|---|---|---|---|
0.00 | 98.44 | 8.01 | 57.38 | 12.80 | 1390879 | 2571.496 | 1572 | 9.820 | 2581.315 |
0.00 | 97.66 | 0.00 | 78.87 | 12.80 | 1393725 | 2576.784 | 1575 | 9.839 | 2586.623 |
98.44 | 0.00 | 14.04 | 5.83 | 12.80 | 1396559 | 2582.082 | 1579 | 9.866 | 2591.948 |
98.44 | 0.00 | 0.00 | 26.41 | 12.80 | 1399394 | 2587.274 | 1582 | 9.885 | 2597.159 |
98.44 | 98.44 | 0.00 | 50.75 | 12.80 | 1402230 | 2592.506 | 1585 | 9.904 | 2602.410 |
98.44 | 0.00 | 0.00 | 84.37 | 12.80 | 1405077 | 2597.808 | 1588 | 9.925 | 2607.733 |
0.00 | 98.44 | 0.00 | 5.19 | 12.80 | 1407926 | 2603.108 | 1592 | 9.952 | 2613.059 |
0.00 | 98.44 | 58.17 | 29.80 | 12.80 | 1410770 | 2608.314 | 1595 | 9.973 | 2618.287 |
99.22 | 0.00 | 0.00 | 54.14 | 12.80 | 1413606 | 2613.582 | 1598 | 9.992 | 2623.574 |
98.44 | 0.00 | 0.00 | 78.18 | 12.80 | 1416444 | 2618.881 | 1601 | 10.012 | 2628.893 |
0.00 | 97.66 | 0.00 | 7.36 | 12.80 | 1419275 | 2624.167 | 1605 | 10.038 | 2634.205 |
0.00 | 99.22 | 0.00 | 31.04 | 12.80 | 1422125 | 2629.391 | 1608 | 10.057 | 2639.448 |
0.00 | 98.44 | 0.00 | 60.41 | 12.80 | 1424974 | 2634.636 | 1611 | 10.077 | 2644.714 |
0.00 | 98.44 | 0.00 | 84.72 | 12.80 | 1427825 | 2639.929 | 1614 | 10.094 | 2650.024 |
0.00 | 97.66 | 0.00 | 12.32 | 12.80 | 1430679 | 2645.204 | 1618 | 10.119 | 2655.323 |
0.00 | 98.44 | 12.05 | 39.31 | 12.80 | 1433539 | 2650.442 | 1621 | 10.141 | 2660.583 |
86.81 | 0.00 | 0.00 | 67.40 | 12.80 | 1436392 | 2655.743 | 1624 | 10.156 | 2665.899 |
99.22 | 0.00 | 0.00 | 95.25 | 12.80 | 1439244 | 2661.071 | 1627 | 10.175 | 2671.246 |
98.44 | 0.00 | 0.00 | 24.63 | 12.80 | 1442090 | 2666.305 | 1631 | 10.201 | 2676.506 |
0.00 | 99.22 | 0.00 | 52.86 | 12.80 | 1444945 | 2671.546 | 1634 | 10.222 | 2681.769 |
98.44 | 0.00 | 0.00 | 80.38 | 12.80 | 1447802 | 2676.850 | 1637 | 10.241 | 2687.091 |
0.00 | 87.50 | 0.00 | 4.22 | 12.80 | 1450658 | 2682.196 | 1641 | 10.268 | 2692.464 |
99.22 | 0.00 | 0.00 | 33.22 | 12.80 | 1453507 | 2687.386 | 1644 | 10.290 | 2697.676 |
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
WeakHashMap
InheritableThreadLocal
Java Agent
Java Agent规范
Java SE 6 新特性: Instrumentation 新功能
Creation, dynamic loading and instrumentation with javaagents
JavaAgent加载机制分析
Javassist
Getting Started with Javassist
Shade插件
Maven的Shade插件