ThreadLocal

这篇文章写的好了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_第1张图片

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这种便利性,就能简化编程逻辑。

你可能感兴趣的:(ThreadLocal)