ThreadLocal 和神奇的数字 0x61c88647

get操作

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

分析get操作可以发现,ThreadLocal对象本身并不存储值,而是作为ThreadLocalMap(TLM)的key值,而后者则存在Thread对象中,因而每个线程都有自己的TLM。、
此处可见事实上ThreadLocal的实现和Java的线程Thread是深度绑定的。

set操作

ThreadLocal的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. }

进入ThreadLocal.ThreadLocalMap#set方法内:

  1. private void set(ThreadLocal<?> key, Object value) {
  2. // We don't use a fast path as with get() because it is at
  3. // least as common to use set() to create new entries as
  4. // it is to replace existing ones, in which case, a fast
  5. // path would fail more often than not.
  6. Entry[] tab = table;
  7. int len = tab.length;
  8. int i = key.threadLocalHashCode & (len-1);
  9. for (Entry e = tab[i];
  10. e != null;
  11. e = tab[i = nextIndex(i, len)]) {
  12. ThreadLocal<?> k = e.get();
  13. if (k == key) {
  14. e.value = value;
  15. return;
  16. }
  17. if (k == null) {
  18. replaceStaleEntry(key, value, i);
  19. return;
  20. }
  21. }
  22. tab[i] = new Entry(key, value);
  23. int sz = ++size;
  24. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  25. rehash();
  26. }

其中,Entry结构体的实现相对特殊:

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

它应用了WeakReference,这使得当前ThreadLocal不再被引用时,下一次GC就会被清除掉,因此tab中的元素有过时(stale)的概念。

从代码可以看到:

  • ThreadLocalMap是用过线性探测方法构建的哈希表
  • Entry的实现使用了WeakReference
  • 每次插入后得到更新后的表大小sz,之后算法会做约log(sz)次的启发式搜索以删除过时的表项,当无法删除且新大小超过既定阈值时,会对哈希表进行扩容。

    使用弱引用的原因

    如果将弱引用转换为强引用,当不再使用ThreadLocal对象时,期内不得ThreadLocalMap对象中的非空Entry仍然持有ThreadLocal的强引用,如果不将它们全部删除,ThreadLocal对象就无法被回收。
    使用弱引用可以防止上述循环引用,因为弱引用不会影响所引用对象的GC周期

    仍然存在的内存泄漏可能

    当ThreadLocal对象被置null后,TLM还通过数组持有Entry的引用,此时TLM中key(ThreadLocal对象)已经消失,但value也即存储的值仍然被Entry也即TLM持有,这种情况将产生内存泄漏。
    解决的方法是,在使用完ThreadLocal对象后,调用ThreadLocal#remove方法,将所有Entry数组表项主动置null并清楚对ThreadLocal的引用,从而使得Entry对象(及其包含的存储的真正的数据)能够被GC。