1 ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。
2 ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。
3 ThreadLocal本身不存储任何值,存在当前线程的ThreadLocalMap中
4 类似 Map 的结构存储变量。每个Thread里面都有一个ThreadLocal.ThreadLocalMap成员变量,也就是说每个线程通过ThreadLocal.ThreadLocalMap与ThreadLocal相绑定。**

1 ThreadLocal

  1. //获取值
  2. public T get() {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null) {
  8. @SuppressWarnings("unchecked")
  9. T result = (T)e.value;
  10. return result;
  11. }
  12. }
  13. return setInitialValue();
  14. }
  15. //初始化
  16. private T setInitialValue() {
  17. T value = initialValue();
  18. Thread t = Thread.currentThread();
  19. ThreadLocalMap map = getMap(t);
  20. if (map != null)
  21. map.set(this, value);
  22. else
  23. createMap(t, value);
  24. return value;
  25. }
  26. //绑定map到当前线程
  27. //设置值
  28. public void set(T value) {
  29. Thread t = Thread.currentThread();
  30. ThreadLocalMap map = getMap(t);
  31. if (map != null)
  32. map.set(this, value);
  33. else
  34. createMap(t, value);
  35. }
  36. //获取线程关联的ThreadLocalMap
  37. ThreadLocalMap getMap(Thread t) {
  38. return t.threadLocals;
  39. }
  40. //创建ThreadLocalMap
  41. void createMap(Thread t, T firstValue) {
  42. t.threadLocals = new ThreadLocalMap(this, firstValue);
  43. }

1.1 直接调用,没有set,返回值

返回空,如果重写了 intValue(),则返回这个对象。

2ThreadLocalMap

ThreadLocal.ThreadLocalMap内部类,维护着每个线程自己的多个ThreadLocal变量, key为当前的threadLocal实例(ThreadLocal本身不存值,只是引用),value为设置的值 (值存放在ThreadLocal.ThreadLocalMap.Entry实例中)。关键定义如下:

  1. static class ThreadLocalMap {
  2. //存储数据的数组
  3. private Entry[] table;
  4. //获取Entry,key的类型为ThreadLocal
  5. private Entry getEntry(ThreadLocal<?> key) {
  6. int i = key.threadLocalHashCode & (table.length - 1);
  7. Entry e = table[i];
  8. if (e != null && e.get() == key)
  9. return e;
  10. else
  11. return getEntryAfterMiss(key, i, e);
  12. }
  13. }
  14. /**
  15. * Set the resize threshold to maintain at worst a 2/3 load factor.
  16. */
  17. private void setThreshold(int len) {
  18. threshold = len * 2 / 3;
  19. }

Entry类是ThreadLocalMap的静态内部类,用于存储数据。key为ThreadLocal本身的弱引用,值为set进来的值。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. / The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Entry类继承了WeakReference>,即每个Entry对象都有一个ThreadLocal本身的弱引用(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。

3 内存泄漏的原因

1 Entry的key是个WeakReference弱引用的ThreadLocal对象,会被垃圾回收,回收后,key会变为null,但value还存在,而又无法通过已经变为null的key索引到value,因此value所在内存便无法使用,又无法回收,导致内存泄露。
2 ThreadLocalMap(即位于Thread中的变量)threadLocals本身的生命周期与线程一致,即使ThreadLocal本身弱引用已经回收,但value还存在于ThreadLocalMap中的Entry中,导致内存泄露

4 如果绑定了ThreadLocal的线程,提交到线程池?

线程,一般不会消亡,ThreadLocalMap,相当一个全局变量,ThreadLocal则会被反复创建,导致内存泄漏

5 如何避免泄漏(被问到过)

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

2、JDK建议ThreadLocal定义为private static(只有一个实例不会反复创建对象),这样ThreadLocal的弱引用问题则不存在了
3 get()、set() remove 都会删除key为null的value;

6ThreadLocalMap 为什么采用开放定址法

闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突。开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。

7 get()、set() remove()都会清除?(被问到过)

7.1 set()

1、获取key的hash对应槽位

2、判断当前槽位是否已被占用,若已被占用,则判断key值是否相同,是则直接替换;否则则判断槽位中的

  1. Entrykey值是否为null,为null则走**replaceStaleEntry**方法;
  2. key不相同或不为null则一直往下找,直到遇到空槽位或key相同或key值为null的槽位

3、若直接找到为空的槽位,则放入并清空部分槽位并判断是否需要扩容

7.2 get()

get,直接定位到ThreadLocalMap.getEntry,直接上源码

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e; — 没有冲突时直接返回,没有清entry
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

  1. while (e != null) {<br /> ThreadLocal<?> k = e.get();<br /> if (k == key)<br /> return e;<br /> if (k == null)<br /> expungeStaleEntry(i); -- 同上,清理i两边空槽之间的keynull的值,<br /> 如果有两个及以上的keynullentry,则调用cleanSomeSlots<br /> else<br /> i = nextIndex(i, len);<br /> e = tab[i];<br /> }<br /> return null;<br />}<br /> <br />-- 同上,清理i两边空槽之间的keynull的值,<br /> 如果有两个及以上的keynullentry,则调用cleanSomeSlots

7.3 remove

private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); — 找到key并将entry.key设为null
expungeStaleEntry(i); — ,
如果有两个及以上的key为null的entry,则调用cleanSomeSlots
return;
}
}
}
说明:不能保证全部清除,但会清除当前key

7.4 总结

总结:除了map扩容时会遍历整个数组进行清除外,其他方法都不能保证全部清除掉所有key为null的entry,除非线程本身被垃圾收集器回收,但现在用的最多的还是线程池,虽然大部分entry和value会被清理,但还会有部分一直存在内存中,所以也不能杜绝内存泄露,最好还是用完后手动remove为好。

8 总结

每个 Thread 里都含有一个 ThreadLocalMap 的成员变量,这种机制将 ThreadLocal 和线程巧妙地绑定在了一起,即可以保证无用的 ThreadLocal 被及时回收,不会造成内存泄露,又可以提升性能。假如我们把 ThreadLocalMap 做成一个 Map 类型的 Map,那么它存储的东西将会非常多(相当于一张全局线程本地变量表),这样的情况下用线性探测法解决哈希冲突的问题效率会非常差。而 JDK 里的这种利用 ThreadLocal 作为 key,再将 ThreadLocalMap 与线程相绑定的实现,完美地解决了这个问题。

什么时候无用的 Entry 会被清理:
1 Thread 结束的时候
2 插入元素时,发现 staled entry,则会进行替换并清理
3 插入元素时,ThreadLocalMap 的 size 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容
4 调用 ThreadLocalMap 的 remove 方法或set(null) 。

尽管不会造成内存泄露,但是可以看到无用的 Entry 只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。

特别地,当应用线程池的时候,由于线程池的线程一般会复用,Thread 不结束,这时候用完更需要 remove 了。

1 那key为什么不使用强引用?
如果key使用强引用,即使调用threadLocalA = null,此时线程中threadLocalMap中仍然持有threadLocal实例的引用,threadLocalA实例仍 然不会被GC回收,造成异常情况

2 value为什么不使用弱引用?
value只存在thread引用->堆区thread实例->threadLocalMap->entryTable->entry->value这一条引用链,假设value为弱引用,则GC后会被回收,再也无法通过ThreadLocal.get()方法获取value值

总的来说,对于多线程资源共享的问题,同步机制采用了 以时间换空间 的方式,而 ThreadLocal 则采用了 以空间换时间 的方式。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。