ThreadLocal父子线程传递实现方案

古城微笑少年丶 2022-11-09 12:35 231阅读 0赞
  1. 介绍InheritableThreadLocal之前,假设对 ThreadLocal 已经有了一定的理解,比如基本概念,原理,如果没有,可以参考:ThreadLocal源码分析解密.在讲解之前我们先列举有关ThreadLocal的几个关键点
  2. 每一个Thread线程都有属于自己的ThreadLocalMap,里面有一个弱引用的Entry(ThreadLocal,Object),如下
  3. Entry(ThreadLocal k, Object v) {
  4. super(k);
  5. value = v;
  6. }
  7. ThreadLocalget值的时候,首先通过Thread.currentThread得到当前线程,然后拿到这个线程的ThreadLocalMap。再传递当前ThreadLocal对象(结合上一点)。取得Entry中的value
  8. set值的时候同理,更改的是当先线程的ThreadLocalMapEntrykey为当前Threadlocal对象的value
  9. Threadlocal bug
  10. 如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?比如会有以下的这种代码的实现。由于ThreadLocal的实现机制,在子线程中get时,我们拿到的Thread对象是当前子线程对象,那么他的ThreadLocalMapnull的,所以我们得到的value也是null
  11. final ThreadLocal threadLocal=new ThreadLocal(){
  12. @Override
  13. protected Object initialValue() {
  14. return "xiezhaodong";
  15. }
  16. };
  17. new Thread(new Runnable() {
  18. @Override
  19. public void run() {
  20. threadLocal.get();//NULL
  21. }
  22. }).start();
  23. InheritableThreadLocal实现
  24. 那其实很多时候我们是有子线程获得父线程ThreadLocal的需求的,要如何解决这个问题呢?这就是InheritableThreadLocal这个类所做的事情。先来看下InheritableThreadLocal所做的事情。
  25. public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  26. protected T childValue(T parentValue) {
  27. return parentValue;
  28. }
  29. /**
  30. * 重写Threadlocal类中的getMap方法,在原Threadlocal中是返回
  31. *t.theadLocals,而在这么却是返回了inheritableThreadLocals,因为
  32. * Thread类中也有一个要保存父子传递的变量
  33. */
  34. ThreadLocalMap getMap(Thread t) {
  35. return t.inheritableThreadLocals;
  36. }
  37. /**
  38. * 同理,在创建ThreadLocalMap的时候不是给t.threadlocal赋值
  39. *而是给inheritableThreadLocals变量赋值
  40. *
  41. */
  42. void createMap(Thread t, T firstValue) {
  43. t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  44. }
  45. 以上代码大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有东西都已经不在原来的t.thradLocals里面,而是在一个新的t.inheritableThreadLocals变量中了。下面是Thread类中两个变量的定义
  46. /* ThreadLocal values pertaining to this thread. This map is maintained
  47. * by the ThreadLocal class. */
  48. ThreadLocal.ThreadLocalMap threadLocals = null;
  49. /*
  50. * InheritableThreadLocal values pertaining to this thread. This map is
  51. * maintained by the InheritableThreadLocal class.
  52. */
  53. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  54. QInheritableThreadLocal是如何实现在子线程中能拿到当前父线程中的值的呢?
  55. A:一个常见的想法就是把父线程的所有的值都copy到子线程中。
  56. 下面来看看在线程new Thread的时候线程都做了些什么?
  57. private void init(ThreadGroup g, Runnable target, String name,
  58. long stackSize, AccessControlContext acc) {
  59. //省略上面部分代码
  60. if (parent.inheritableThreadLocals != null)
  61. //这句话的意思大致不就是,copy父线程parent的map,创建一个新的map赋值给当前线程的inheritableThreadLocals。
  62. this.inheritableThreadLocals =
  63. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  64. //ignore
  65. }
  66. 而且,在copy过程中是浅拷贝,keyvalue都是原来的引用地址
  67. private ThreadLocalMap(ThreadLocalMap parentMap) {
  68. Entry[] parentTable = parentMap.table;
  69. int len = parentTable.length;
  70. setThreshold(len);
  71. table = new Entry[len];
  72. for (int j = 0; j < len; j++) {
  73. Entry e = parentTable[j];
  74. if (e != null) {
  75. ThreadLocal key = e.get();
  76. if (key != null) {
  77. Object value = key.childValue(e.value);
  78. Entry c = new Entry(key, value);
  79. int h = key.threadLocalHashCode & (len - 1);
  80. while (table[h] != null)
  81. h = nextIndex(h, len);
  82. table[h] = c;
  83. size++;
  84. }
  85. }
  86. }
  87. }
  88. 恩,到了这里,大致的解释了一下InheritableThreadLocal为什么能解决父子线程传递Threadlcoal值的问题。
  89. 在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量
  90. 在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为nullcopy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去
  91. 因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值
  92. so,在最开始的代码示例中,如果把ThreadLocal对象换成InheritableThreadLocal对象,那么get到的字符会是“xiezhaodong”而不是NULL
  93. InheritableThreadLocal还有问题吗?
  94. 问题场景
  95. 我们在使用线程的时候往往不会只是简单的new Thrad对象,而是使用线程池,当然线程池的好处多多。这里不详解,既然这里提出了问题,那么线程池会给InheritableThreadLocal带来什么问题呢?我们列举一下线程池的特点:
  96. 为了减小创建线程的开销,线程池会缓存已经使用过的线程
  97. 生命周期统一管理,合理的分配系统资源
  98. 对于第一点,如果一个子线程已经使用过,并且会set新的值到ThreadLocal中,那么第二个task提交进来的时候还能获得父线程中的值吗?比如下面这种情况(虽然是线程,用sleep尽量让他们串行的执行):
  99. final InheritableThreadLocal<Span> inheritableThreadLocal = new InheritableThreadLocal<Span>();
  100. inheritableThreadLocal.set(new Span("xiexiexie"));
  101. //输出 xiexiexie
  102. Object o = inheritableThreadLocal.get();
  103. Runnable runnable = new Runnable() {
  104. @Override
  105. public void run() {
  106. System.out.println("========");
  107. inheritableThreadLocal.get();
  108. inheritableThreadLocal.set(new Span("zhangzhangzhang");
  109. inheritableThreadLocal.get();
  110. }
  111. };
  112. ExecutorService executorService = Executors.newFixedThreadPool(1);
  113. executorService.submit(runnable);
  114. TimeUnit.SECONDS.sleep(1);
  115. executorService.submit(runnable);
  116. TimeUnit.SECONDS.sleep(1);
  117. System.out.println("========");
  118. Span span = inheritableThreadLocal.get();
  119. }
  120. static class Span {
  121. public String name;
  122. public int age;
  123. public Span(String name) {
  124. this.name = name;
  125. }
  126. }
  127. 输出的会是
  128. xiexiexie
  129. ========
  130. xiexiexie
  131. zhangzhangzhang
  132. ========
  133. zhangzhangzhang
  134. zhangzhangzhang
  135. ========
  136. xiexiexie
  137. 造成这个问题的原因是什么呢,下图大致讲解一下整个过程的变化情况,如图所示,由于B任务提交的时候使用了,A任务的缓存线程,A缓存线程的InheritableThreadLocal中的value已经被更新成了”zhangzhangzhang“。B任务在代码内获得值的时候,直接从t.InheritableThreadLocal中获得值,所以就获得了线程A中心设置的值,而不是父线程中InheritableThreadLocal的值。
  138. soInheritableThreadLocal还是不能够解决线程池当中获得父线程中ThreadLocal中的值。
  139. 造成问题的原因
  140. 那么造成这个问题的原因是什么呢?如何让任务之间使用缓存的线程不受影响呢?实际原因是,我们的线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用现在的localMap
  141. 解决方案
  142. 如果我们能够,在使用完这个线程的时候清除所有的localMap,在submit新任务的时候在重新重父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题github:alibaba/transmittable-thread-local
  143. transmittable-thread-local实现原理
  144. 如何使用
  145. 这个库最简单的方式是这样使用的,通过简单的修饰,使得提交的runable拥有了上一节所述的功能。具体的API文档详见github,这里不再赘述
  146. TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
  147. parent.set("value-set-in-parent");
  148. Runnable task = new Task("1");
  149. // 额外的处理,生成修饰了的对象ttlRunnable
  150. Runnable ttlRunnable = TtlRunnable.get(task);
  151. executorService.submit(ttlRunnable);
  152. // Task中可以读取, 值是"value-set-in-parent"
  153. String value = parent.get();
  154. 原理简述
  155. 这个方法TtlRunnable.get(task)最终会调用构造方法,返回的是该类本身,也是一个Runable,这样就完成了简单的装饰。最重要的是在run方法这个地方。
  156. public final class TtlRunnable implements Runnable {
  157. private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
  158. private final Runnable runnable;
  159. private final boolean releaseTtlValueReferenceAfterRun;
  160. private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
  161. //从父类copy值到本类当中
  162. this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
  163. this.runnable = runnable;//提交的runable,被修饰对象
  164. this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
  165. }
  166. /**
  167. * wrap method {@link Runnable#run()}.
  168. */
  169. @Override
  170. public void run() {
  171. Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
  172. if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
  173. throw new IllegalStateException("TTL value reference is released after run!");
  174. }
  175. //装载到当前线程
  176. Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
  177. try {
  178. runnable.run();//执行提交的task
  179. } finally {
  180. //clear
  181. TransmittableThreadLocal.restoreBackup(backup);
  182. }
  183. }
  184. }
  185. 在上面的使用线程池的例子当中,如果换成这种修饰的方式进行操作,B任务得到的肯定是父线程中ThreadLocal的值,解决了在线程池中InheritableThreadLocal不能解决的问题。
  186. 更新父线程ThreadLocal值?
  187. 如果线程之间出了要能够得到父线程中的值,同时想更新值怎么办呢?在前面我们有提到,当子线程copy父线程的ThreadLocalMap的时候是浅拷贝的,代表子线程Entry里面的value都是指向的同一个引用,我们只要修改这个引用的同时就能够修改父线程当中的值了,比如这样:
  188. @Override
  189. public void run() {
  190. System.out.println("========");
  191. Span span= inheritableThreadLocal.get();
  192. System.out.println(span);
  193. span.name="liuliuliu";//修改父引用为liuliuliu
  194. inheritableThreadLocal.set(new Span("zhangzhangzhang"));
  195. System.out.println(inheritableThreadLocal.get());
  196. }
  197. 这样父线程中的值就会得到更新了。能够满足父线程ThreadLocal值的实时更新,同时子线程也能共享父线程的值。不过场景倒是不是很常见的样子。

发表评论

表情:
评论列表 (有 0 条评论,231人围观)

还没有评论,来说两句吧...

相关阅读