前言

首先看看 JDK 文档的描述:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)

例如,以下类生成对每个线程唯一的局部标识符。 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCur

java.lang.ThreadLocal 不是 1.5 新加入的类,在 1.2 的时候就已经存在 java 类库的类,但该类的作用非常的大,所以我们也要剖析一下他的源码,也要验证关于该类的一些争论,比如内存泄漏。

1. 如何使用?

该类有4个方法方法需要关注:

  1. public class ThreadLocal<T> {
  2. public T get();
  3. public void set(T value);
  4. public void remove();
  5. protected T initialValue();

get() : 返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用  initialValue() 方法返回的值。

set():将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。

remove() :移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取, 且这期间当前线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue 方法。则不会对该线程再调用 initialValue 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又调用了 remove() ,则可能再次调用此方法。

initialValue():返回此线程局部变量的当前线程的“初始值”。线程第一次使用 get() 方法变量时将调用此方法,但如果线程之前调用了 set(T) 方法,

我们还是来个例子:

  1. package cn.think.in.java.lock.tools;
  2. public class ThreadLocalDemo {
  3. public static void main(String[] args) {
  4. ThreadLocal<String> local = new MyThreadLocal<>();
  5. System.out.println(local.get());
  6. local.set("hello");
  7. System.out.println(local.get());
  8. local.remove();
  9. System.out.println(local.get());
  10. }
  11. }
  12. class MyThreadLocal<T> extends ThreadLocal<T> {
  13. @Override
  14. protected T initialValue() {
  15. return (T) "world";
  16. }
  17. }

运行结果

  1. world
  2. hello
  3. world

上面的代码中,我们重写了 ThreadLocal 的 initialValue 方法,返回了一个字符串 “world”,第一次调用 get 方法返回了该值,而我们然后又调用 set 方法设置了 hello 字符串,再次调用 get 方法,此时返回的就是刚刚set 的值 ---- hello,然后我们调用remove 方法,删除 hello,再次调用 get 方法,返回了 initialValue 方法中的 world。

从这个流程中,我们已经知道了该类的用法,那么我们就看看源码是如何实现的。

get() 源码剖析

  1. public T get() {
  2. // 获取当前线程
  3. Thread t = Thread.currentThread();
  4. // 获取当前线程的 ThreadLocalMap 对象
  5. ThreadLocalMap map = getMap(t);
  6. // 如果map不是null,将 ThreadlLocal 对象作为 key 获取对应的值
  7. if (map != null) {
  8. ThreadLocalMap.Entry e = map.getEntry(this);
  9. // 如果该值存在,则返回该值
  10. if (e != null) {
  11. T result = (T)e.value;
  12. return result;
  13. }
  14. }
  15. // 如果上面的逻辑没有取到值,则从 initialValue 方法中取值
  16. return setInitialValue();
  17. }

楼主在该方法中写了注释,主要逻辑是从 当前线程中取出 一个类似 Map 的对象,

map 中 key是 ThreadLocal 对象,value 则是我们设置的值。如果 该 map中没有,则从 initialValue 方法中取。

我们继续看看,map 的真实面目:

就是这个map,这个map 持有一个 Entry 数组,Entry 继承了 WeakReference ,也就是弱引用,如果一个对象具有弱引用,在GC线程扫描内存区域的过程中,不管当前内存空间足够与否,都会回收内存。这个特性我们之后再说。

总的来说,每个线程对象中都有一个 ThreadLocalMap 属性,该属性存储 ThreadLocal 为 key ,值则是我们调用 ThreadLocal 的 set 方法设置的,也就是说,一个ThreakLocal 对象对应一个 value。

还没完,我们看看 getEntry 方法:

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);
  3. Entry e = table[i];
  4. if (e != null && e.get() == key)
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);
  8. }

如果hash 没有冲突,直接返回 对应的值,如果冲突了,调用 getEntryAfterMiss 方法。

getEntryAfterMiss 源码:

  1. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. while (e != null) {
  5. ThreadLocal<?> k = e.get();
  6. if (k == key)
  7. return e;
  8. if (k == null)
  9. expungeStaleEntry(i);
  10. else
  11. i = nextIndex(i, len);
  12. e = tab[i];
  13. }
  14. return null;
  15. }

该方法会循环所有的元素,直到找到 key 对应的 entry,如果发现了某个元素的 key 是 null,顺手调用 expungeStaleEntry 方法清理 所有 key 为 null 的 entry。

那么 set 方法是怎么样的呢?

3. set() 方法源码剖析

源码如下:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }

该方法同样先得到当前线程,然后根据当前线程得到线程的 ThreadLocalMap 属性,如果 Map 为null, 则创建一个Map ,并将值放置到Map中,否则,直接将值放置到Map中。

先看看 createMap(Thread t, T firstValue) 方法:

  1. void createMap(Thread t, T firstValue) {
  2. t.threadLocals = new ThreadLocalMap(this, firstValue);
  3. }
  4. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  5. table = new Entry[INITIAL_CAPACITY]; // 默认长度16
  6. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 得到下标
  7. table[i] = new Entry(firstKey, firstValue); // 创建一个entry对象并插入数组
  8. size = 1; // 设置长度属性为1
  9. setThreshold(INITIAL_CAPACITY); 设置阀值== 16 * 2 / 3 == 10
  10. }

这个方法很简单,楼主已经写了详细的注释。就是创建一个16长度的entry 数组,设置阀值为10,注意,再resize 的时候,并不是10,而是 10 - 10 / 4,也就是 8,负载因子为 0.5,和 HashMap 是不同的。

我们再看看 map.set(ThreadLocal<?> key, Object value) 方法如何实现的:

  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. // 根据 ThreadLocal 的 HashCode 得到对应的下标
  5. int i = key.threadLocalHashCode & (len-1);
  6. // 首先通过下标找对应的entry对象,如果没有,则创建一个新的 entry对象
  7. // 如果找到了,但key冲突了或者key是null,则将下标加一(加一后如果小于数组长度则使用该值,否则使用0),
  8. // 再次尝试获取对应的 entry,如果不为null,则在循环中继续判断key 是否重复或者k是否是null
  9. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
  10. ThreadLocal<?> k = e.get();
  11. // key 相同,则覆盖 value
  12. if (k == key) {
  13. e.value = value;
  14. return;
  15. }
  16. // 如果key被 GC 回收了(因为是软引用),则创建一个新的 entry 对象填充该槽
  17. if (k == null) {
  18. replaceStaleEntry(key, value, i);
  19. return;
  20. }
  21. }
  22. // 创建一个新的 entry 对象
  23. tab[i] = new Entry(key, value);
  24. // 长度加一
  25. int sz = ++size;
  26. // 如果没有清楚多余的entry 并且数组长度达到了阀值,则扩容
  27. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  28. rehash();
  29. }

这里楼主刚开始有一个奇怪的地方,为什么这里和 HashMap 处理 Hash 冲突的方式不一样,楼主后来查询资料,才明白,HashMap 的Hash冲突方法是拉链法,即用链表来处理,而 ThreadLocalMap 处理Hash冲突采用的是线性探测法,即这个槽不行,就换下一个槽,直到插入为止。但是该方法有一个问题,就是,如果整个数组都冲突了,就会不停的循环,导致死循环,虽然这种几率很小。

我们继续往下。

如果 k == null,表示 ThreadLocal 被GC回收了,那么就调用 replaceStaleEntry 方法重新生成一个 entry,不过该方法没有我说的那么简单,我们来看看:

  1. private void replaceStaleEntry(ThreadLocal<?> key, Object value,
  2. int staleSlot) {
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. Entry e;
  6. // Back up to check for prior stale entry in current run.
  7. // We clean out whole runs at a time to avoid continual
  8. // incremental rehashing due to garbage collector freeing
  9. // up refs in bunches (i.e., whenever the collector runs).
  10. int slotToExpunge = staleSlot;
  11. for (int i = prevIndex(staleSlot, len);
  12. (e = tab[i]) != null;
  13. i = prevIndex(i, len))
  14. if (e.get() == null)
  15. slotToExpunge = i;
  16. // Find either the key or trailing null slot of run, whichever
  17. // occurs first
  18. for (int i = nextIndex(staleSlot, len);
  19. (e = tab[i]) != null;
  20. i = nextIndex(i, len)) {
  21. ThreadLocal<?> k = e.get();
  22. // If we find key, then we need to swap it
  23. // with the stale entry to maintain hash table order.
  24. // The newly stale slot, or any other stale slot
  25. // encountered above it, can then be sent to expungeStaleEntry
  26. // to remove or rehash all of the other entries in run.
  27. if (k == key) {
  28. e.value = value;
  29. tab[i] = tab[staleSlot];
  30. tab[staleSlot] = e;
  31. // Start expunge at preceding stale entry if it exists
  32. if (slotToExpunge == staleSlot)
  33. slotToExpunge = i;
  34. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  35. return;
  36. }
  37. // If we didn't find stale entry on backward scan, the
  38. // first stale entry seen while scanning for key is the
  39. // first still present in the run.
  40. if (k == null && slotToExpunge == staleSlot)
  41. slotToExpunge = i;
  42. }
  43. // If key not found, put new entry in stale slot
  44. tab[staleSlot].value = null;
  45. tab[staleSlot] = new Entry(key, value);
  46. // If there are any other stale entries in run, expunge them
  47. if (slotToExpunge != staleSlot)
  48. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  49. }

该方法可以说有点复杂,楼主看了很久,真的没想到 ThreadLocal 这么复杂。。。。。如同该方法名称,该方法会删除陈旧的 entyr,什么是陈旧的呢,就是 ThreadLocal 为 null 的 entry,会将 entry key 为 null 的对象设置为null。核心的方法就是 expungeStaleEntry(int);

整体逻辑就是,通过线性探测法,找到每个槽位,如果该槽位的key为相同,就替换这个value;如果这个key 是null,则将原来的entry 设置为null,并重新创建一个entry。

不论如何,只要走到了这里,都会清除所有的 key 为null 的entry,也就是说,当hash 冲突的时候并且对应的槽位的key值是null,就会清除所有的key 为null 的entry。

我们回到 set 方法。如果 hash 没有冲突,也会调用 cleanSomeSlots 方法,该方法同样会清除无用的 entry,也就是 key 为null 的节点。我们看看代码:

  1. private boolean cleanSomeSlots(int i, int n) {
  2. boolean removed = false;
  3. Entry[] tab = table;
  4. int len = tab.length;
  5. do {
  6. i = nextIndex(i, len);
  7. Entry e = tab[i];
  8. if (e != null && e.get() == null) {
  9. n = len;
  10. removed = true;
  11. i = expungeStaleEntry(i);
  12. }
  13. } while ( (n >>>= 1) != 0);
  14. return removed;
  15. }

该方法会遍历所有的entry,并判断他们的key,如果key是null,则调用 expungeStaleEntry 方法,也就是清除 entry。最后返回 true。

如果返回了 false ,说明没有清除,并且 size 还 大于等于 10 ,就需要 rahash,该方法如下:

  1. private void rehash() {
  2. expungeStaleEntries();
  3. // Use lower threshold for doubling to avoid hysteresis
  4. if (size >= threshold - threshold / 4)
  5. resize();
  6. }

首先会调用 expungeStaleEntries 方法,该方法会清除无用的 entry,我们之前说过了,同时,也会对 size 变量做减法,如果减完之后,size 还大于 8,则调用 resize 方法做真正的扩容。

resize 方法如下:

  1. private void resize() {
  2. Entry[] oldTab = table;
  3. int oldLen = oldTab.length;
  4. int newLen = oldLen * 2;
  5. Entry[] newTab = new Entry[newLen];
  6. int count = 0;
  7. for (int j = 0; j < oldLen; ++j) {
  8. Entry e = oldTab[j];
  9. if (e != null) {
  10. ThreadLocal<?> k = e.get();
  11. if (k == null) {
  12. e.value = null; // Help the GC
  13. } else {
  14. int h = k.threadLocalHashCode & (newLen - 1);
  15. while (newTab[h] != null)
  16. h = nextIndex(h, newLen);
  17. newTab[h] = e;
  18. count++;
  19. }
  20. }
  21. }
  22. setThreshold(newLen);
  23. size = count;
  24. table = newTab;
  25. }

该方法会直接扩容为原来的2倍,并将老数组的数据都移动到 新数组,size 变量记录了里面有多少数据,最后设置扩容阀值为 2/3。

所以说,扩容分为2个步骤,当长度达到了容量的2/3,就会清理无用的数据,如果清理完之后,长度还大于等于阀值的3/4,那么就做真正的扩容。而不是网上很多人说的达到了 2/3 就扩容。这里的误区就是扩容之前需要清理。清理完之后再做判断。

可以看到,每次调用set 方法都会进行清理工作。实际上,如果使用 get 方法,当对应的 entry 的key为null 的时候,也会进行清理。

4. remove 方法源码剖析

  1. private void remove(ThreadLocal<?> key) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. if (e.get() == key) {
  9. e.clear();
  10. expungeStaleEntry(i);
  11. return;
  12. }
  13. }
  14. }

通过线性探测法找到 key 对应的 entry,调用 clear 方法,将 ThreadLocal 设置为null,调用 expungeStaleEntry 方法,该方法顺便会清理所有的 key 为 null 的 entry。

5. Thread 线程退出时清理 ThreadLocal

Thread 的exit 方法:

  1. private void exit() {
  2. if (group != null) {
  3. group.threadTerminated(this);
  4. group = null;
  5. }
  6. /* Aggressively null out all reference fields: see bug 4006245 */
  7. target = null;
  8. /* Speed the release of some of these resources */
  9. threadLocals = null;
  10. inheritableThreadLocals = null;
  11. inheritedAccessControlContext = null;
  12. blocker = null;
  13. uncaughtExceptionHandler = null;
  14. }

可以看到,该方法会将线程相关的所有属性变量全部清除。包括 threadLocals。

总结

楼主开始以为这个类的代码不会很难,想来楼主太天真了。从源码中我们可以看到,ThreadLocal 类的作者无时无刻都在想着如何去除那些 key 为 null 的 元素,为什么?因为只要线程不退出,这些变量都会一直留在线程中。

但是,Java 中有线程池的技术,也就是说,线程基本不会退出,因此,就需要手动去删除这些变量。如果你在线程中放置了一个大大的对象,使用完几次后没有清除(调用 remove 方法),该对象将会一直留在线程中。造成了内存泄漏。

为什么要使用弱引用呢?我们假设一下,不用弱引用,如果我们使用的 ThreadLocal 的变量是个局部变量,并设置到了线程中,当这个方法结束时,我们没有调用 remove 方法,而 Map 中 key 不是弱引用,那么该变量将会一直存在!!!

如果使用了弱引用,就算你没有调用 remove 方法,GC 也会清除掉 Map 中的引用,同时,ThreadLocal 也会通过对 key 是否为 null 进行判断,从而防止内存泄漏。

这里我们重新总结一下:ThreadLocal 的作者之所以使用弱引用,是担心程序员使用了局部变量的ThreadLocal 并且没有调用 remove 方法,这将导致没有结束的线程发生内存泄漏。使用弱引用,即使程序员没有删除,GC 也会将该变量设置为null,ThrealLocal 通过判断 key 是否为 null 来清除无用数据。防止内存泄漏。

当然,如果你使用的是静态变量,并且使用结束后没有设置为 null, ThrealLocal 是无法自动删除的,因此需要调用 remove 方法。

那么,ThrealLocal 什么时候会自动回收呢?当调用 remove 方法的时候(废话),当调用 get 方法并且 hash 冲突了的时候(情况很少),调用 set 方法时 hash 冲突了,调用 set 方法时正常插入。注意,调用 set 方法时,如果是覆盖操作,则不会执行清理。

我们正常使用 ThreadLocal 都是静态变量,也是 JDK 建议的例子,所以一定要手动调用 remove 方法,或者使用完毕后置为 null。反之,你可以碰运气不好,JDK 可能会帮你删,比如在你 set 的时候(也就是我们上面说的那几种情况),如果运气不好,就会永远存在线程中,导致内存泄漏。

所以,强烈建议手动调用 remove 方法。

并发编程之 ThreadLocal 源码剖析的更多相关文章

  1. 并发编程之 ConcurrentLinkedQueue 源码剖析

    前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...

  2. 并发编程之 CopyOnWriteArrayList 源码剖析

    前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...

  3. Java并发编程之ThreadLocal源码分析

    ## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4>  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...

  4. 并发编程之 LinkedBolckingQueue 源码剖析

    前言 JDK 1.5 之后,Doug Lea 大神为我们写了很多的工具,整个 concurrent 包基本都是他写的.也为我们程序员写好了很多工具,包括我们之前说的线程池,重入锁,线程协作工具,Con ...

  5. 并发编程之 AQS 源码剖析

    前言 JDK 1.5 的 java.util.concurrent.locks 包中都是锁,其中有一个抽象类 AbstractQueuedSynchronizer (抽象队列同步器),也就是 AQS, ...

  6. 并发编程之ThreadLocal源码分析

    当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...

  7. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

  8. 并发编程之 Condition 源码分析

    前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...

  9. 并发编程之 Semaphore 源码分析

    前言 并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore--信号量,关于他的使用请查看往期文章并发编程之 线程 ...

随机推荐

  1. UniGUI的TUniLoginForm窗口自定义背景色和背景图片

    雨田家园 UniGUI的TUniLoginForm窗口自定义背景色 uniGUI的TUniLoginForm类创建的登录窗口默认是不带颜色,可以自定义css风格来改变背景颜色. 一般是通过在UniSe ...

  2. Android-Kotlin-印章类

    上一篇博客介绍了,Android-Kotlin-枚举enum: 由于枚举 和 印章类 有相似之处,所以两者对比一下: Kotlin的枚举,重点区分的数据本身 Kotlin的印章类,重点区分的是数据类型 ...

  3. WPF如何设置Image.Source为资源图片

    img.Source = new BitmapImage(new Uri(path,UriKind.RelativeOrAbsolute));

  4. 【BZOJ3280】 小R的烦恼(费用流,建模)

    有很浓厚的熟悉感?餐巾计划问题? 不就是多了几个医院,相当于快洗部和慢洗部开了分店. 考虑建图: 如果把每一天拆成两个点,一个表示需求,另一个表示拥有的话. 显然就是一个两边的图,考虑如果我现在有人, ...

  5. 201621123018《Java程序设计》第7周学习报告

    1. 本周学习总结 1.1 思维导图:Java图形界面总结 2.书面作业 1. GUI中的事件处理 1.1 写出事件处理模型中最重要的几个关键词. 事件.事件源. 事件监听器.事件处理方法 1.2 任 ...

  6. XSS 跨站脚本攻击 的防御解决方案

    虽然说在某些特殊情况下依然可能会产生XSS,但是如果严格按照此解决方案则能避免大部分XSS攻击. 原则:宁死也不让数据变成可执行的代码,不信任任何用户的数据,严格区数据和代码. XSS的演示 Exam ...

  7. 基本数据类型补充 set集合 深浅拷贝

    一.基本数据类型补充 1,关于int和str在之前的学习中已经介绍了80%以上了,现在再补充一个字符串的基本操作: li = ['李嘉诚','何炅','海峰','刘嘉玲'] s = "_&q ...

  8. Django(命名URL和URL反向解析)

    day67 参考: https://www.cnblogs.com/liwenzhou/articles/8271147.html#autoid-1-4-0 反向解析URL             本 ...

  9. TCP连接详解

    一. 连接过程示意图 二. 建立TCP连接 2.1 三次握手 第一次握手:建立连接.客户端发送连接请求报文段,将SYN置为1,Sequence Number为x:然后,客户端进入SYN_SEND状态, ...

  10. Docker三剑客之Docker Compose

    一.什么是Docker Compose Compose 项目是Docker官方的开源项目,负责实现Docker容器集群的快速编排,开源代码在https://github.com/docker/comp ...