ThreadLocal父子线程数据传递解决方案
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal父子线程数据传递解决方案相关的知识,希望对你有一定的参考价值。
参考技术A 在阅读前,您需要对ThreadLocal用法有一定了解。如果不了解,请参考ThreadLocal用法相关文章。这个需求或许也很常见,最开始,我们可能会想到InheritableThreadLocal,这是由Jdk为我们提供,他是ThreadLocal的子类,使用方法和ThreadLocal完全相同。
但是这里有几个坑:
1、手动通过线程池创建线程可能会造成get值为null。
2、项目中我们往往会使用线程池,如果主线程使用的是缓存线程池(比如SpringMvc),线程会复用,当线程执行完毕后本次操作后,再次执行新的任务时候,ThreadLocal内部数据并没有被清除。
3、ThreadLocal父子线程之间数据拷贝默认是浅拷贝,这也就意味如果我们多个线程可能会引用同一个内存地址,造成多个线程访问一个对象,轻者会造成线程不安全,重者甚至会ThreadLocal数据被修改成非预期结果。
这些坑本人亲身体验过,血的教训总结出来,那么如何很好的解决这些问题呢?这边给出一个终级解决方案,使用阿里Transmittable ThreadLocal,可以很好的解决上面这些问题。
具体使用方法可以参见文档: https://github.com/alibaba/transmittable-thread-local
如果使用后发现在缓存线程池下仍然会出现ThreadLocal没有被清理干净的问题,可可尝试重写TransmittableThreadLocal afterExecute方法,在此方法中清除线程数据。
那么通过阿里的TransmittableThreadLocal 如何解决浅拷贝问题呢?这里它提供了一个copy方法,TransmittableThreadLocal 在进行对象拷贝的时候会调用copy方法,我们只需要重写这个方法,让他变量深复制即可。
最后,建议大家不要轻意使用ThreadLocal,能不用尽量不要去使用,一旦在缓存线程池下如果数据没有清理干净会很麻烦,不方便问题排查,而且ThreadLocal有一定情况下会有造成内存泄露的风险,一定要慎用!慎用!慎用!但并不意味着禁用!
ThreadLocal详解
1.作用
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
ThreadLocal最适合按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到(线程内传递数据 而不用利用方法参数显式传递)
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
2.要注意的地方
ThreadLocal并不能解决并发问题。
ThreadLocal就像一个管理类或代理类,间接去操作真正的数据。
真实的数据是存储在ThreadLocalMap中的,而每个线程都有一个属性threadLocals,也就是线程拥有的ThreadLocalMap。
ThreadLocalMap内是用Entry来存储数据的,key是ThreadLocalMap实例,value就是真实的数据。
每个线程只有一个ThreadLocalMap, 可以存多个ThreadLocal。
线程内从ThreadLocal内获取数据时(get), 要先set,否则获取到的是null,后续自然会报NPE。当然源码也提供了initialValue方法,我们只要重写一下,自然就可以避免这个NPE了
3.原理和源码
ThreadLocal类核心方法set、get、initialValue、withInitial、setInitialValue、remove:
1 /** 2 * Returns the current thread‘s "initial value" for this 3 * thread-local variable. This method will be invoked the first 4 * time a thread accesses the variable with the {@link #get} 5 * method, unless the thread previously invoked the {@link #set} 6 * method, in which case the {@code initialValue} method will not 7 * be invoked for the thread. Normally, this method is invoked at 8 * most once per thread, but it may be invoked again in case of 9 * subsequent invocations of {@link #remove} followed by {@link #get}. 10 * 11 * <p>This implementation simply returns {@code null}; if the 12 * programmer desires thread-local variables to have an initial 13 * value other than {@code null}, {@code ThreadLocal} must be 14 * subclassed, and this method overridden. Typically, an 15 * anonymous inner class will be used. 16 * 17 * @return the initial value for this thread-local 18 */ 19 protected T initialValue() { 20 return null; 21 } 22 23 /** 24 * Creates a thread local variable. The initial value of the variable is 25 * determined by invoking the {@code get} method on the {@code Supplier}. 26 * 27 * @param <S> the type of the thread local‘s value 28 * @param supplier the supplier to be used to determine the initial value 29 * @return a new thread local variable 30 * @throws NullPointerException if the specified supplier is null 31 * @since 1.8 32 */ 33 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { 34 return new SuppliedThreadLocal<>(supplier); 35 } 36 37 /** 38 * Creates a thread local variable. 39 * @see #withInitial(java.util.function.Supplier) 40 */ 41 public ThreadLocal() { 42 } 43 44 /** 45 * Returns the value in the current thread‘s copy of this 46 * thread-local variable. If the variable has no value for the 47 * current thread, it is first initialized to the value returned 48 * by an invocation of the {@link #initialValue} method. 49 * 50 * @return the current thread‘s value of this thread-local 51 */ 52 public T get() { 53 Thread t = Thread.currentThread(); 54 ThreadLocalMap map = getMap(t); 55 if (map != null) { 56 ThreadLocalMap.Entry e = map.getEntry(this); 57 if (e != null) { 58 @SuppressWarnings("unchecked") 59 T result = (T)e.value; 60 return result; 61 } 62 } 63 return setInitialValue(); 64 } 65 66 /** 67 * Variant of set() to establish initialValue. Used instead 68 * of set() in case user has overridden the set() method. 69 * 70 * @return the initial value 71 */ 72 private T setInitialValue() { 73 T value = initialValue(); 74 Thread t = Thread.currentThread(); 75 ThreadLocalMap map = getMap(t); 76 if (map != null) 77 map.set(this, value); 78 else 79 createMap(t, value); 80 return value; 81 } 82 83 /** 84 * Sets the current thread‘s copy of this thread-local variable 85 * to the specified value. Most subclasses will have no need to 86 * override this method, relying solely on the {@link #initialValue} 87 * method to set the values of thread-locals. 88 * 89 * @param value the value to be stored in the current thread‘s copy of 90 * this thread-local. 91 */ 92 public void set(T value) { 93 Thread t = Thread.currentThread(); 94 ThreadLocalMap map = getMap(t); 95 if (map != null) 96 map.set(this, value); 97 else 98 createMap(t, value); 99 } 100 101 /** 102 * Removes the current thread‘s value for this thread-local 103 * variable. If this thread-local variable is subsequently 104 * {@linkplain #get read} by the current thread, its value will be 105 * reinitialized by invoking its {@link #initialValue} method, 106 * unless its value is {@linkplain #set set} by the current thread 107 * in the interim. This may result in multiple invocations of the 108 * {@code initialValue} method in the current thread. 109 * 110 * @since 1.5 111 */ 112 public void remove() { 113 ThreadLocalMap m = getMap(Thread.currentThread()); 114 if (m != null) 115 m.remove(this); 116 }
createMap和getMap
1 /** 2 * Create the map associated with a ThreadLocal. Overridden in 3 * InheritableThreadLocal. 4 * 5 * @param t the current thread 6 * @param firstValue value for the initial entry of the map 7 */ 8 void createMap(Thread t, T firstValue) { 9 t.threadLocals = new ThreadLocalMap(this, firstValue); 10 } 11 12 13 ThreadLocalMap getMap(Thread t) { 14 return t.threadLocals; 15 } 16
Thread.threadLocals和Thread.inheritableThreadLocals
1 public 2 class Thread implements Runnable { 3 /*...其他属性...*/ 4 5 /* ThreadLocal values pertaining to this thread. This map is maintained 6 * by the ThreadLocal class. */ 7 ThreadLocal.ThreadLocalMap threadLocals = null; 8 9 /* 10 * InheritableThreadLocal values pertaining to this thread. This map is 11 * maintained by the InheritableThreadLocal class. 12 */ 13 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
4.Thread同步机制的比较
ThreadLocal和线程同步机制相比有什么优势呢?
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
上述参考:枫之逆 原文:https://blog.csdn.net/lufeng20/article/details/24314381
5.内存泄漏问题
ThreadLocal 实现原理
ThreadLocal
的实现是这样的:每个Thread
维护一个 ThreadLocalMap
映射表,这个映射表的 key
是 ThreadLocal
实例本身,value
是真正需要存储的 Object
。
也就是说 ThreadLocal
本身并不存储值,它只是作为一个 key
来让线程从 ThreadLocalMap
获取 value
。值得注意的是图中的虚线,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key
的,弱引用的对象在 GC 时会被回收。
ThreadLocal
为什么会内存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用
static
的ThreadLocal
,延长了ThreadLocal
的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。 - 分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么就会导致内存泄漏。
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal
使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal
内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完
ThreadLocal
,都调用它的remove()
方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal
,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal
就跟加锁完要解锁一样,用完就清理。
上面关于ThreadLocal内在泄漏的分析 摘自:http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
关于内存泄漏的问题他还有一篇实例分析 => ThreadLocal 内存泄露的实例分析
6. ThreadLocal应用场景
1、数据库连接池实现
2、有时候ThreadLocal也可以用来避免一些参数传递,通过ThreadLocal来访问对象
3、在某些情况下提升性能和安全,如:SimpleDateFormat
参考:https://blog.csdn.net/u012834750/article/details/71646700
7.关于ThreadLocal在Spring中的应用
https://www.cnblogs.com/fishisnow/p/6396989.html
https://www.cnblogs.com/youzhibing/p/6690341.html
以上是关于ThreadLocal父子线程数据传递解决方案的主要内容,如果未能解决你的问题,请参考以下文章