多线程传递Context multi-thread context(MTC)

在使用线程池等会缓存线程的组件情况下,完成多线程的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的整体构架 :
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();

这种使用方式的时序图 :
MTC时序图

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<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.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方式为什么是应用代码无侵入的?
MTC应用场景图

按框架图,把前面示例代码操作可以分成下面几部分:

读取信息设置到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:

S0S1EOPYGCYGCTFGCFGCTGCT
0.0097.660.008.3312.7014709352636.215410.2292636.444
97.660.000.0017.1812.7014739682640.597410.2292640.825
98.440.000.0025.4712.7014770202645.265410.2292645.493
96.880.0033.0434.0312.7014800682650.149410.2292650.378
0.0097.6614.0141.8212.7014831132655.262410.2292655.490
0.0097.6674.0750.2512.7014861492660.596410.2292660.825
96.880.000.0058.3212.7014891702666.135410.2292666.364
98.440.0026.0767.0512.7014921622671.841410.2292672.070
0.0097.660.0076.5012.7014951392677.809410.2292678.038
0.0097.660.0085.9512.7014980912683.994410.2292684.222
96.880.000.0096.5012.7015010382690.454410.2292690.683
97.660.000.007.9612.7015040542695.583420.2332695.816
0.0097.660.0017.4612.7015070992700.009420.2332700.241
0.0097.660.0026.9712.7015101332704.652420.2332704.885
97.660.000.0036.5712.7015131582709.592420.2332709.825
0.0097.660.0045.5912.7015161672714.738420.2332714.971
98.440.000.0054.4912.7015191662720.109420.2332720.342
0.0098.440.0063.5212.7015221412725.688420.2332725.921
0.0097.6684.1872.0012.7015251392731.579420.2332731.812
0.0098.4420.0480.1012.7015281212737.680420.2332737.913
0.0097.6628.0687.7012.7015310932743.991420.2332744.224
0.0098.440.0095.6312.7015340552750.508420.2332750.741
97.660.000.004.7512.7015370622756.196430.2392756.435

MtContextThreadLocal的每分钟GC时间是5.29s,FGC次数是3.27:

S0S1EOPYGCYGCTFGCFGCTGCT
0.0098.448.0157.3812.8013908792571.49615729.8202581.315
0.0097.660.0078.8712.8013937252576.78415759.8392586.623
98.440.0014.045.8312.8013965592582.08215799.8662591.948
98.440.000.0026.4112.8013993942587.27415829.8852597.159
98.4498.440.0050.7512.8014022302592.50615859.9042602.410
98.440.000.0084.3712.8014050772597.80815889.9252607.733
0.0098.440.005.1912.8014079262603.10815929.9522613.059
0.0098.4458.1729.8012.8014107702608.31415959.9732618.287
99.220.000.0054.1412.8014136062613.58215989.9922623.574
98.440.000.0078.1812.8014164442618.881160110.0122628.893
0.0097.660.007.3612.8014192752624.167160510.0382634.205
0.0099.220.0031.0412.8014221252629.391160810.0572639.448
0.0098.440.0060.4112.8014249742634.636161110.0772644.714
0.0098.440.0084.7212.8014278252639.929161410.0942650.024
0.0097.660.0012.3212.8014306792645.204161810.1192655.323
0.0098.4412.0539.3112.8014335392650.442162110.1412660.583
86.810.000.0067.4012.8014363922655.743162410.1562665.899
99.220.000.0095.2512.8014392442661.071162710.1752671.246
98.440.000.0024.6312.8014420902666.305163110.2012676.506
0.0099.220.0052.8612.8014449452671.546163410.2222681.769
98.440.000.0080.3812.8014478022676.850163710.2412687.091
0.0087.500.004.2212.8014506582682.196164110.2682692.464
99.220.000.0033.2212.8014535072687.386164410.2902697.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插件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值