前言
ThreadLocal类作为线程相关的比较重要的类,在开发和面试中经常被用到或者问到。比如之前我们实现的分布式锁中,在基于ZK的分布式锁中就有被用到。当时它的作用是记录下在ZK中当前锁的目录和前一个锁的目录。从而达到有序释放和获取锁的目的。
简单介绍
ThreadLocal能保证其中的数据只属于当前线程,而不能被其他线程所访问,但是一个ThreadLocal变量只能保存一份的数据,比如
1 | private ThreadLocal<String> curPath = new ThreadLocal<>(); // 当前锁的目录 |
像这种保存了一个K和V,但是一个线程其实可以创建多个ThreadLocal对象,来保存不同的数据,所以在Thread类中有一个threadLocals的成员变量来保存所有的threadlocal。它是一个ThreadLocalMap的变量,后续再做源码分析。所以我们可以通过这个map来保存和获取本线程中所有的threadlocal。
1 | ThreadLocal.ThreadLocalMap threadLocals = null; |
源码分析
以下分析基于jdk1.8
ThreadLocalMap结构
ThreadLocalMap类似于一个HashMap的结构,保存了当前线程中所有threadlocal的KV信息。它是ThreadLocal中的一个静态内部类。
1 | private static final int INITIAL_CAPACITY = 16; |
从构造函数可以看出,ThreadLocalMap是一个Entry类型的数组,其中初始容量INITIAL_CAPACITY=16,阈值为长度的2/3。
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
其中Map中的每一个实体Entry是继承了弱引用的,这边关注到它的key对象,key是一个ThreadLocal类型的对象,也就是ThreadLocal把自己当成了一个key存入Map中。并且我们可以看到,这个Map是没有next指针的,也就是说它无法像HashMap一样用链地址法解决哈希冲突,另外,由于Entry是继承弱引用的,所以这边的key是一个弱引用。也就是说,只要key==null的时候,这个key即被回收。
总结一下,这里可能会产生两个问题。一个是ThreadLocalMap是如何解决哈希冲突的,另一个是当key被回收掉之后,value其实还是存在的,那么如何解决value的问题。
给出ThreadLocalMap中set()函数并给出相应的注释
1 | private void set(ThreadLocal<?> key, Object value) { |
总结一下:有三种情况存在。1.若位置i上面已经有一个key值并且等于我现在要新set的key,则直接覆盖掉。2.当前Entry不为空,但是key为空,则调用replaceStaleEntry()删除掉前后某个范围内key为空的不空Entry。并将新值存入。3.若没有找到相同key的Entry或者没有key为空的Entry,也就是找到了一个null,那么就直接把新值放进去。
内存泄露问题
之前提到ThreadLocalMap中的key是一个弱引用,也就是当key==null的时候一旦发生GC就会被回收。如果ThreadLocal一直持续运行,那么这个Entry对象的value就一直得不到回收,发生内存泄露的问题。那该如何避免呢?在调用ThreadLocal的get()和set()方法的时候,会清除key值为null的对象Entry。也就是replaceStaleEntry()这个方法,简单来看一下里面的逻辑。
1 |
|
正是在ThreadLocal的get和set方法中都有replaceStaleEntry()函数的调用,所以正常情况下是不存在内存溢出的问题的,但是如果我们没有调用get和set的时候就有可能面临内存溢出的问题,所以一般来说在不再使用的时候调用remove()方法可以加快垃圾回收,避免内存溢出。
疑问:当没有强引用指向ThreadLocal中的ThreadLocalMap,即它被回收掉之后,不也没有内存溢出的问题了吗?
但是有一种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap 和里面的元素是不会回收掉的。
ThreadLocal中的方法
上面提到了ThreadLocal中的set和get方法。
1 | public void set(T value) { |
其他问题
ThreadLocalMap 采用开放地址法原因
- ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了
- ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?
InheritableThreadLocal解决父子线程传递Threadlcoal值的问题。
1.在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量
2.在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去
3.因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值
参考: