ThreadLocal 类的作用就是实现每一个线程都有自己的专属本地变量。
public class Demo01 implements Runnable { // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); public static void main(String[] args) throws InterruptedException { Demo01 obj = new Demo01(); for(int i=0 ; i<10; i++){ Thread t = new Thread(obj, ""+i); Thread.sleep(new Random().nextInt(1000)); t.start(); } } @Override public void run() { System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } // formatter 被重新设置,但是其它的线程不受影响,它们仍然持有原始的那一份副本 formatter.set(new SimpleDateFormat()); System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); } }
上述代码中,Thread-0 使用 set 方法改变了 formatter,但是这个改变只对自己生效。其它线程看到的仍然是最开始的值。
我们一般调用 withInitial 方法进行 ThreadLocal 对象的初始化,可以看到它的返回值的实际类型为 SuppliedThreadLocal :
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { // 返回的是 SuppliedThreadLocal<> 类型的对象 return new SuppliedThreadLocal<>(supplier); }
继续跟进,SuppliedThreadLocal 不过是 ThreadLocal 的静态内部类,继承了 ThreadLocal 结构也很简单:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { // 我们初始化的数据就存储在这个 supplier 对象中 private final Supplier<? extends T> supplier; // withInitial 方法调用的构造器 SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } // 这是一个被重载的方法,很重要 @Override protected T initialValue() { // 返回 supplier 中我们实际提供的对象 return supplier.get(); } }
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
我们的 formatter 对象的真实类型应该是 SuppliedThreadLocal,这点很重要。
先来看看 Thread 类的源码,每个Thread 都有两个 ThreadLocalMap 类型的字段,且访问权限为 friendly,即同包下的类可以访问:
摆上几个关键类的关系:
当我们调用 ThreadLocal 的 get 方法时:
会首先使用 getMap 方法获取当前线程的 ThreadLocalMap,由于 ThreadLocal 和 Thread 类在同一个包下(java/lang),所以可以直接访问 Thread 对象(当前线程)的 threadLocals 字段:
如果当前线程的 ThreadLocalMap 的字段为 null,那么就需要 setInitialValue 函数来进行当前 Thread 中 ThreadLocalMap 字段的初始化:
private T setInitialValue() { T value = initialValue(); // 1,2 Thread t = Thread.currentThread(); // t = 当前线程 ThreadLocalMap map = getMap(t); // 获取当前线程的 map,为 null if (map != null) map.set(this, value); else createMap(t, value); // 创建 map return value; // 返回 null 或者已经初始化的值 } // 1 // ThreadLocal 中的 initialValue 方法 // 不是用 withInitial 初始化的 ThreadLocal 调用 protected T initialValue() { return null; } // 2 // SuppliedThreadLocal 中的 initialValue // 用 withInitial 初始化的 ThreadLocal 调用 @Override protected T initialValue() { return supplier.get(); // 返回我们存储的对象 } // t 是当前线程,firstValue 为 null void createMap(Thread t, T firstValue) { // 赋值给 Thread 对象 t.threadLocals = new ThreadLocalMap(this, firstValue); }
如果当前线程的 ThreadLocalMap 字段不为 null,则直接使用 ThreadLocalMap 的 get 方法获取 ThreadLocalMap.Entry,get 方法的参数就是当前的 ThreadLocal 对象,而该方法的结果是一个一个弱引用对象:
// ThreadLocal 类中的 ThreadLocalMap 内部类的 Entry 内部类 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; // 就是我们赋予给 ThreadLocal 对象的值 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
// 设置 ThreadLocal 的值 public void set(T value) { // 获取当前线程,作为 KEY Thread t = Thread.currentThread(); // 以当前线程为 key 查找 map ThreadLocalMap map = getMap(t); if (map != null) // 如果当前线程存在对应的 map,将新的对象设置进去,key 为当前的 ThreadLocal 对象 map.set(this, value); else // 否则就创建新的 ThreadLocalMap,并且设置值 createMap(t, value); }
也就是说每个 Thread 对象中(线程)存储着一个 map(ThreadLocalMap),这个 map 的 key 值为 ThreadLocal 对象,value 为我们想赋予的值。
当我们使用 ThreadLocal 的 get 方法时,ThreadLocal 对象会根据当前线程获取到当前线程的 map,并且再以自己(ThreadLocal 对象)为 key 找到我们所赋予的值。
在谈及 ThreadLocal 内存泄露问题之前,我们需要先看一下 ThreadLocalMap 的具体实现。
表项的 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 的 createMap 方法会直接初始化一个 ThreadLocalMap:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 创建新的 map 表项 table = new Entry[INITIAL_CAPACITY]; // 计算当前 ThreadLocal 对象的 hash 值 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 塞进去 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
ThreadLocal 的 get 方法将会调用 getEntry 方法:
private Entry getEntry(ThreadLocal<?> key) { // 计算 hash 值 int i = key.threadLocalHashCode & (table.length - 1); // 直接获取目标对象 Entry e = table[i]; // 目标对象不为 null if (e != null && e.get() == key) // 返回 Entry return e; else return getEntryAfterMiss(key, i, e); }
较为复杂,暂不涉及
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
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; // 获取下一个 Entry e = tab[i = nextIndex(i, len)]) { // e 是 Reference 对象,get 方法将获取弱引用的 ThreadLocal 对象 if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
ThreadLocalMap
存储表项的数据结构就是一个简单的数组,通过 ThreadLocal 的 hash 值来计算索引并进行存储。ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。
所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。
假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。
使用完 ThreadLocal
方法后 最好手动调用remove()
方法。