ThreadLocal 分析

从功能上来说,它类似一个全局的Map,key是线程。不同线程get时拿到的都是专属于自己的那个对象,互相隔离,完全不存在并发问题。

典型的使用方式

// 摘自 j.u.c.ThreadLocalRandom
private static final ThreadLocal<ThreadLocalRandom> localRandom =  // ThreadLocal对象都是static的,全局共享
    new ThreadLocal<ThreadLocalRandom>() {      // 初始值
        protected ThreadLocalRandom initialValue() {
            return new ThreadLocalRandom();
        }
};

localRandom.get();      // 拿当前线程对应的对象
localRandom.put(...);   // put

典型使用场景

  1. 用空间换并发度;
  2. 在线程范围内传参,如 hibernate 的 session;

实现

一个非常自然想法是用一个线程安全的 Map<Thread,Object> 实现:

class ThreadLocal {
  private Map values = Collections.synchronizedMap(new HashMap());

  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }

  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }
}

但这是非常naive的:

  1. ThreadLocal本意是避免并发,用一个全局Map显然违背了这一初衷;
  2. Thread当key,除非手动调用remove,否则即使线程退出了 1)该Thread对象无法回收; 2)该线程在所有ThreadLocal中对应的value也无法回收。

JDK 的实现刚好是反过来的:
Alt text

每个Thread对象内都存在一个ThreadLocal.ThreadLocalMap对象,保存着该线程所有用到的ThreadLocal及其value。

ThreadLocalMap是定义在ThreadLocal类内部的私有类,它是采用“开放定址法”解决冲突的hashmap。key是ThreadLocal对象。当调用某个ThreadLocal对象的getput方法时,首先会从当前线程中取出ThreadLocalMap,然后查找对应的value:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);     //拿到当前线程的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);    // 以该ThreadLocal对象为key取value
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

现在来看看它的哈希策略。所有ThreadLocal对象共享一个AtomicInteger对象nextHashCode用于计算hashcode,一个新对象产生时它的hashcode就确定了,算法是从0开始,以HASH_INCREMENT = 0x61c88647为间隔递增,这是ThreadLocal唯一需要同步的地方。根据hashcode定位桶的算法是将其与数组长度-1进行与操作:key.threadLocalHashCode & (table.length - 1)

0x61c88647这个魔数是怎么确定的呢?

ThreadLocalMap的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,上述算法中使用0x61c88647可以让hash的结果在2的n次方内尽可能均匀分布,减少冲突的概率。具体原因我也不知道,不过这是一个好的参考。

内存管理

ThreadLocalMap.Entry继承自WeakReference,是 key(ThreadLocal 对象) 和 value 的容器:

static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

一旦某个ThreadLocal对象没有强引用了,它在所有线程内部的ThreadLocalMap中的key都将被GC掉(此时value还未回收),在map后续的get/set中会探测到key被回收的entry,将其 value 设置为 null 以帮助GC,因此 value 在 key 被 GC 后可能还会存活一段时间,但最终也会被回收。这个过程和java.util.WeakHashMap的实现几乎是一样的。

一旦线程退出,Thread对象被回收了,内部ThreadLocalMap中的value也可以被回收了。但是线程池的场景需要注意,线程池中的线程可能永远不会退出,只会阻塞,如果队列中的某个任务向ThreadLocalput了一个对象,但任务结束后并未清空,那么这个对象在ThreadLocal的下一次put或clear前永远不会被GC;这种情况下,假如线程池有200个线程,那么一个ThreadLocal最多可能造成200个对象的内存泄露。

Loading Disqus comments...
目录