前言 ThreadLocal,简单翻译过来就是本地线程,但是直接这么翻译很难理解ThreadLocal的作用,如果换一种说法,可以称为线程本地存储。简单来说,就是ThreadLocal为共享变量在每个线程中都创建一个副本,每个线程可以访问自己内部的副本变量。这通常会比其它可选方法更简单、更快。例如,当在服务器上执行事务时,可能需要在多个方法和对象中访问事务上下文。ThreadLocal变量通常用于保存这种信息,以避免使用显式参数传递。
由于ThreadLocal的底层是ThreadLocalMap,这是一个静态内部类,用于存储每个线程的ThreadLocal变量。每个线程都有一个自己的ThreadLocalMap。ThreadLocalMap是一个定制的哈希映射,只用于维护线程本地值。它的键是ThreadLocal对象,值是线程本地变量的值。我们下面也会着重分析下这个静态内部类的源码。
简单用法 下面的例子中,我们创建了三个线程,每个线程都设定了自己的值,第三个线程会调用remove方法清除值。从结果我们可以看到每个线程的操作都是独立的,值并没有串掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class ThreadLocalTest { static ThreadLocal<Integer> threadLocal = new ThreadLocal <>(); public static void main (String[] args) { new Thread (() -> { threadLocal.set(1 ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println("子线程的值1: " + threadLocal.get()); }).start(); new Thread (() -> { threadLocal.set(2 ); try { Thread.sleep(500 ); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println("子线程的值2: " + threadLocal.get()); }).start(); new Thread (() -> { threadLocal.set(3 ); threadLocal.remove(); System.out.println("子线程的值3: " + threadLocal.get()); }).start(); } }
程序结果:
子线程的值3: null 子线程的值2: 2 子线程的值1: 1
关键属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger (); private static final int HASH_INCREMENT = 0x61c88647 ;private static int nextHashCode () { return nextHashCode.getAndAdd(HASH_INCREMENT); }
构造器 1 2 3 4 5 6 7 public ThreadLocal () {} public static <S> ThreadLocal<S> withInitial (Supplier<? extends S> supplier) { return new SuppliedThreadLocal <>(supplier); }
常用方法 设置属性 通过set方法来设置属性
1 2 3 4 5 6 7 8 9 10 public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { map.set(this , value); } else { createMap(t, value); } }
访问属性 访问属性也是从ThreadLocalMap中获取。需要注意的是,我们可以指定未查询到数据时的返回值,默认是返回null,如果要返回其它值,只要重写initialValue()即可。所以我们在调用get()方法时并不是单纯的查询操作,在未查询到数据时它会有写的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public T get () { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { ThreadLocalMap.Entry e = map.getEntry(this ); if (e != null ) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue () { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { map.set(this , value); } else { createMap(t, value); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this ); } return value; } ThreadLocalMap getMap (Thread t) { return t.threadLocals; }
移除属性 从ThreadLocal中移除当前线程的值,主要操作也是在ThreadLocalMap中。
1 2 3 4 5 6 7 public void remove () { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) { m.remove(this ); } }
总结 总的来说,ThreadLocal自身的操作逻辑其实是很清晰的,它的逻辑基本都是维护在自己的静态内部类ThreadLocalMap中。
ThreadLocalMap解析 简介 ThreadLocalMap是维护在ThreadLocal类中的一个静态内部类,它用于存储每个线程的ThreadLocal变量。每个线程都有一个自己的ThreadLocalMap。ThreadLocalMap是一个定制的哈希映射,只用于维护线程本地值。它的键是ThreadLocal对象,值是线程本地变量的值。
关键属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final int INITIAL_CAPACITY = 16 ;private Entry[] table;private int size = 0 ;private int threshold; private void setThreshold (int len) { threshold = len * 2 / 3 ; }
构造器 这里注意这个点,ThreadLocalMap的存储容器为一个Entry类型的数组,默认容量为16。这里的寻址算法是ThreadLocal的Hash值和数组容量减去一后的值做“与”操作后得出来的值。
1 2 3 4 5 6 7 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry [INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new Entry (firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); }
关键方法 添加数据 添加数据调用的是set方法,这个方法会先根据当前ThreadLocal的hash值和数组长度-1的值计算这个ThreadLocal在表中的下标,然后从下标处开始遍历数组,如果最后一个元素也不匹配会从头开始遍历。
如果找到具有相同键的条目,它将用新值替换条目的值
如果找到过时的条目,就替换掉这个条目
如果下标处为null,构造一个Entry并放入到这个位置中。同时清理条目后再判断是否触发扩容操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private void set (ThreadLocal<?> key, Object value) { 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.refersTo(key)) { e.value = value; return ; } if (e.refersTo(null )) { replaceStaleEntry(key, value, i); return ; } } tab[i] = new Entry (key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
作为延伸,我们看下ThreadLocalMap是如何替换指定位置过时条目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 private void replaceStaleEntry (ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null ; i = prevIndex(i, len)) if (e.refersTo(null )) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { if (e.refersTo(key)) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return ; } if (e.refersTo(null ) && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null ; tab[staleSlot] = new Entry (key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
接下来我们看下ThreadLocalMap的扩容机制:
其实只是一个简单的扩容机制,大概是构造一个大小是旧数组两倍的新数组,然后遍历就数组,并将其中的内容根据寻址算法重新计算其位置,然后放入即可。
1 2 3 4 5 6 7 8 private void rehash () { expungeStaleEntries(); if (size >= threshold - threshold / 4 ) resize(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private void resize () { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; Entry[] newTab = new Entry [newLen]; int count = 0 ; for (Entry e : oldTab) { if (e != null ) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; } else { int h = k.threadLocalHashCode & (newLen - 1 ); while (newTab[h] != null ) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
查询操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private Entry getEntry (ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1 ); Entry e = table[i]; if (e != null && e.refersTo(key)) return e; else return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss (ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null ) { if (e.refersTo(key)) return e; if (e.refersTo(null )) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null ; }
删除操作 还是先根据寻址算法找到数组的下标,然后从这个下标处开始往后遍历,直到找到条目位置,删除条目后又清理了下过时条目。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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.refersTo(key)) { e.clear(); expungeStaleEntry(i); return ; } } }
内存泄漏问题 具体来说,可能出现内存泄漏的情况有两种:
ThreadLocal对象没有被外部强引用,但是它在ThreadLocalMap中作为键存在。由于ThreadLocalMap的键是对ThreadLocal对象的弱引用,所以在下一次垃圾回收时,ThreadLocal对象会被回收,但是ThreadLocalMap中的对应的值(即变量副本)却无法被回收,因为它仍然被ThreadLocalMap强引用。
长生命周期的线程(例如线程池中的线程)持有ThreadLocalMap,而ThreadLocalMap中的键值对没有被及时移除。这种情况下,即使ThreadLocal对象被外部强引用,ThreadLocalMap中的值(即变量副本)也无法被回收,因为它们被长生命周期的线程持有的ThreadLocalMap强引用。
为了避免ThreadLocal导致的内存泄漏,我们需要注意以下几点:
每次使用完ThreadLocal后,都应该调用其remove方法,将不再使用的变量副本从ThreadLocalMap中移除。
避免在长生命周期的线程(例如线程池中的线程)中使用ThreadLocal,除非你能确保变量副本在不再使用后能被及时移除
避免ThreadLocal对象被无限期地强引用,例如将ThreadLocal对象存储在静态字段中