这篇文章写的好了http://duanqz.github.io/2018-03-15-Java-ThreadLocal
同时这篇文章关于内存泄漏讲的挺好的http://www.importnew.com/22039.html
http://www.iteye.com/topic/103804
ThreadLocal,在没有任何背景知识的情况下,我们从英文单词的意思上理解它:
Thread:跟线程相关。Java语言中,表示线程的类就是Thread,是程序最小的执行单元,多个线程可以并发执行。
Local:本地、局部,与之相对的概念就是远程、全局。Java语言中,通常用Local表示局部变量。
这两个概念一组合,拼成了英文单词ThreadLocal,线程局部?局部线程?究竟要表达什么意思,为什么不叫LocalThread,完全找不着北啊!
笔者搜罗了网上对ThreadLocal的一些解读:
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量,把它命名为ThreadLocalVariable更容易让人理解一些
ThreadLocal并不是用来并发控制访问一个共同对象,而是为了给每个线程分配一个只属于该线程的变量。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突,实现线程间的数据隔离。从线程的角度看,就好像每一个线程都完全拥有该变量
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文
引入ThreadLocal的初衷是为了提供线程内的局部变量,而不是为了解决共享对象的多线程访问问题。实际上,ThreadLocal根本就不能解决共享对象的多线程访问问题
1. ThreadLocal 实现原理
ThreadLocal的实现是这样的:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。值得注意的是图中的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在 GC 时会被回收。
线程类Thread中有一个类型为ThreadLocalMap的变量为threadLocals
ThreadLocalMap是一个映射表,内部实现是一个数组,每一个元素的类型为Entry
Entry就是一个键值对(Key-Value Pair),其 Key 就是ThreadLocal,其 Value 可以是任何对象
1.1 ThreadLocal的主要接口
set(),表示要往当前线程中设置“本地变量”,最终的结果是将变量设置到了线程的映射表。
get(),表示要从当前线程中取出“本地变量”,最终的结果是在当前线程的映射表中,以调用get()方法的ThreadLocal对象为Key,查询出对应的Value。
ThreadLocal的set()和get()方法的主体逻辑算是比较简单了,围绕主体逻辑,还做了一些特殊处理,譬如:线程中的映射表还未初始化时,调用createMap()进行初始化;在映射表中没有获取到Value时,通过setInitialValue()设置一个初始值,这种场景下,只需要实现initialValue()函数就可以了,这种ThreadLocal的使用方式很常见。本文不再展开这些细枝末节的逻辑,读者自行阅读源码即可。
1.2 ThreadLocalMap映射表
ThreadLocal并不是一个存储容器,往ThreadLocal中读(get)和写(set)数据,其实都是将数据保存到了每个线程自己的存储空间。
线程中的存储空间是一个映射表(ThreadLocalMap),TheadLocal其实就是这个映射表每一项的Key,通过ThreadLocal读写数据,其实就是通过Key在一个映射表中读写数据。
上文中图示中,我们见过映射表的结构,它是一个名为table的数组,每一个元素都是Entry对象,而Entry对象包含key和value两个属性,其代码如下所示:
ThreadLocalMap的Entry是WeakReference的子类,这样能保证线程中的映射表的每一个Entry可以被垃圾回收,而不至于发生内存泄露。因为ThreadLocal作为全局的Key,其生命周期很可能比一个线程要长,如果Entry是一个强引用,那么线程对象就一直持有ThreadLocal的引用,而不能被释放。随着线程越来越多,这些不能被释放的内存也就越来越多。
ThreadLocal作为映射表的Key,需要具备唯一的标识,每创建一个新的ThreadLocal,这个标识就变的跟之前不一样了。 如何保证每一个ThreadLocal的唯一性呢?
ThreadLocal内部有一个名为threadLocalHashCode的变量,每创建一个新的ThreadLocal对象,这个变量的值就会增加0x61c88647。 正是因为有这么一个神奇的数字,它能够保证生成的Hash值可以均匀的分布在0~(2^N-1)之间,N是数组长度。 更多关于数字0x61c88647,可以参考Why 0x61c88647?
2. 使用场景
下面来看一个hibernate中典型的ThreadLocal的应用:
Java代码
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
}catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。 显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西
3.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使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
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就跟加锁完要解锁一样,用完就清理。
4.总结
通过上述使用场景可以发现,ThreadLocal确实提供了一种编程手段,本来需要在线程中显示声明的局部变量,像是被ThreadLocal隐藏了起来,当多个线程运行起来时,每个线程都往相同的ThreadLocal中存取所需要的变量就可以了,使用ThreadLocal存取的变量,就像是每个线程自己的局部变量,不受其他线程运行状态的影响。
通过ThreadLocal可以解决多线程读共享数据的问题,因为共享数据会被复制到每个线程,不需要加锁便可同步访问。但ThreadLocal解决不了多线程写共享数据的问题,因为每个线程写的都是自己本线程的局部变量,并没将写数据的结果同步到其他线程。理解了这一点,才能理解所谓的:
ThreadLocal以空间换时间,提升多线程并发的效率。什么意思呢?每个线程都有一个ThreadLocalMap映射表,正是利用了这个映射表所占用的空间,使得多个线程都可以访问自己的这片空间,不用担心考虑线程同步问题,效率自然会高。
ThreadLocal并不是为了解决共享数据的互斥写问题,而是通过一种编程手段,正好提供了并行读的功能。什么意思呢?ThreadLocal并不是万能的,它的设计初衷只是提供一个便利性,使得线程可以更为方便地使用局部变量。
ThreadLocal提供了一种线程全域访问功能,什么意思呢?一旦将一个对象添加到ThreadLocal中,只要不移除它,那么,在线程的生命周期内的任何地方,都可以通过ThreadLocal.get()方法拿到这个对象。有时候,代码逻辑比较复杂,一个线程的代码可能分散在很多地方,利用ThreadLocal这种便利性,就能简化编程逻辑。