ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:
- 存储单个线程上下文信息。比如存储id等;
- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。
下面罗列了几个面试经常会问到的问题,求职者熟记后定可通过面试。
一、ThreadLocal内部数据结构
ThreadLocal里面的实现,主要涉及到以下几个重要类:
1)Thread:大家很熟悉的线程类,一个Thread类自然代表一个线程。
2)ThreadLocal:既然本文是要解析ThreadLocal类,自然就离不开这个类啦~。
3)ThreadLocalMap:可以看成一个HashMap,但是它本身具体的实现并没有实现继承HashMap甚至跟java.util.Map都沾不上一点关系。只是内部的实现跟HashMap类似(通过哈希表的方式存储)。
4)ThreadLocalMap.Entry:把它看成是保存键值对的对象,其本质上是一个WeakReference弱引用对象。
熟记操作流程:
hreadLocal的set(T)函数中,首先是拿到当前线程Thread对象中的ThreadLocalMap对象实例threadLocals ,然后再将需要保存的值保存到threadLocals里面。 换句话说,每个线程引用的ThreadLocal副本值都是保存在当前线程Thread对象里面的。存储结构为ThreadLocalMap类型,ThreadLocalMap保存的键类型为ThreadLocal,值为副本值。
转载请注明转自:https://blog.csdn.net/huachao1001 https://blog.csdn.net/huachao1001/article/details/51970237,引入图说明。
二、为什么使用弱引用?为什么内存泄漏问题?
ThreadLocalMap.Entry:把它看成是保存键值对的对象,其本质上是一个WeakReference弱引用对象。
首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
那么如何有效的避免呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!
还有就是不要放入指向型的对象作为value,不然可能外部还存在强引用,导致ThreadLocal中的value原来所指向的对象不能被GC。。。。。。
四、和线程池一起使用为什么会线程不安全?
使用线程池,归还线程之前要清除ThreadLocalMap,不然再取出该线程的时候,ThreadLocal还会存在。这不仅是内存泄露的问题,业务逻辑都可能会并发错误。可以重写ThreadPoolExecutor.afterExecute方法来处理。
protected void afterExecute(Runnable r, Throwable t) {
// you need to set this field via reflection.
Thread.currentThread().threadLocals = null;
}
或者在使用时完成的业务类里面清理掉。
threadLocal.set(...);
try {
...
} finally {
threadLocal.remove();
}
ThreadLocal最好还是不要和线程池一起使用。ThreadLocal放map也会导致map里面的数据越来越多,一定要清除掉。
五、Hash冲突?
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
默认16数组长度不够时,就扩容,然后再找到合适的位置。建议:每个线程存一个变量,或少量变量,减少Hash冲突的概率。