说明
ThreadLocal 的作用是提供线程内的局部变量,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。
基本使用
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 public class ThreadLocalDemo { public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); public static ThreadLocal<User> threadLocalUser = new ThreadLocal<User>(); public static void main (String args[]) { threadLocal.set(100 ); System.out.println(threadLocal.get()); User user = new User(); user.setName("Tom" ); user.setAge(25 ); threadLocalUser.set(user); System.out.println(threadLocalUser.get()); } static class User { String name; Integer age; @Override public String toString () { return "User [name=" + name + ", age=" + age + "]" ; } } }
源码解析
ThreadLocal 的定义
ThreadLocal 提供了线程局部变量。能使线程中的某个值与保存值的对象关联起来。ThreadLocal 提供了 get 与 set 等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本(即每个线程的 threadLocals 属性),因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。
只要线程处于活动状态并且 Threadocal 实例可以访问,每个线程就拥有对其线程局部变量副本的隐式引用;在一个线程消失之后,线程本地实例的所有副本都会被垃圾收集(除非存在对这些副本的其他引用)。
ThreadLocal 的 hashcode(threadLocalHashCode)是从 0 开始,每新建一个 ThreadLocal,对应的 hashcode 就加 0x61c88647。如下:
1 2 3 4 5 6 7 8 9 10 11 12 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);
ThreadLocalMap 的定义
ThreadLocalMap 是一个自定义哈希映射,仅用于维护线程本地变量值。ThreadLocalMap 是 ThreadLocal 的内部类,主要有一个 Entry 数组,Entry 的 key 为 ThreadLocal,value 为 ThreadLocal 对应的值。每个线程都有一个 ThreadLocalMap 类型的 threadLocals 变量。
1 2 3 4 5 6 7 8 9 static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } }
set()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 getMap (Thread t) { return t.threadLocals; }
先拿到当前线程,再使用 getMap 方法拿到当前线程的 threadLocals 变量
如果 threadLocals 不为空,则将当前 ThreadLocal 作为 key,传入的值作为 value,调用 set 方法(见下文代码块 1 详解)插入 threadLocals。
如果 threadLocals 为空则调用创建一个 ThreadLocalMap,并新建一个 Entry 放入该 ThreadLocalMap, 调用 set 方法的 ThreadLocal 和传入的 value 作为该 Entry 的 key 和 value
注意此处的 threadLocals 变量是一个 ThreadLocalMap,是 Thread 的一个局部变量,因此它只与当前线程绑定。
代码块 1:set 方法
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 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)]) { 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(); }
通过传入的 key 的 hashCode 计算出索引的位置
从索引位置开始遍历,由于不是链表结构,因此通过 nextIndex 方法来寻找下一个索引位置
如果找到某个 Entry 的 key 和传入的 key 相同,则用传入的 value 替换掉该 Entry 的 value。
如果遍历到某个 Entry 的 key 为空,则调用 replaceStaleEntry 方法(见下文代码块 2 详解)
如果通过 nextIndex 寻找到一个空位置(代表没有找到 key 相同的),则将元素放在该位置上
调用 cleanSomeSlots 方法清理 key 为 null 的 Entry,并判断是否需要扩容,如果需要则调用 rehash 方法进行扩容(见下文 rehash 方法详解)。
代码块 2:replaceStaleEntry 方法
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 47 48 49 50 51 52 53 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.get() == null ) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return ; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null ; tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
slotToExpunge 始终记录着需要清除的元素的最前面的位置(即 slotToExpunge 前面的元素是不需要清除的)
从位置 staleSlot 向前遍历,直到遇到 Entry 为空,用 staleSlot 记录最后一个 key 为 null 的索引位置(也就是遍历过位置最前的 key 为 null 的位置)
从位置 staleSlot 向后遍历,直到遇到 Entry 为空,如果遍历到 key 和入参 key 相同的,则将入参的 value 替换掉该 Entry 的 value,并将 i 位置和 staleSlot 位置的元素对换(staleSlot 位置较前,是要清除的元素),遍历的时候判断 slotToExpunge 的值是否需要调整,最后调用 expungeStaleEntry 方法(见下文 expungeStaleEntry 方法详解)和 cleanSomeSlots 方法(见下文代码块 3 详解)清除 key 为 null 的元素。
如果 key 没有找到,则使用入参的 key 和 value 新建一个 Entry,放在 staleSlot 位置
判断是否还有其他位置的元素 key 为 null,如果有则调用 expungeStaleEntry 方法和 cleanSomeSlots 方法清除 key 为 null 的元素
代码块 3:cleanSomeSlots 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private boolean cleanSomeSlots (int i, int n) { boolean removed = false ; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null ) { n = len; removed = true ; i = expungeStaleEntry(i); } } while ( (n >>>= 1 ) != 0 ); return removed; }
从 i 开始,清除 key 为空的 Entry,遍历次数由当前的 table 长度决定,当遍历到一个 key 为 null 的元素时,调用 expungeStaleEntry 清除,并将遍历次数重置。至于为什么使用 table 长度来决定遍历次数,官方给出的解释是这个方法简单、快速,并且效果不错。
get()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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(); }
跟 set 方法差不多,先拿到当前的线程,再使用 getMap 方法拿到当前线程的 threadLocals 变量
如果 threadLocals 不为空,则将调用 get 方法的 ThreadLocal 作为 key,调用 getEntry 方法(见下文代码块 5 详解)找到对应的 Entry。
如果 threadLocals 为空或者找不到目标 Entry,则调用 setInitialValue 方法(见下文代码块 4 详解)进行初始化。
代码块 4:setInitialValue 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 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); return value; }
如果是 threadLocals 为空,创建一个新的 ThreadLocalMap,并将当前的 ThreadLocal 作为 key,null 作为 value,插入到新创建的 ThreadLocalMap,并返回 null。
如果 threadLocals 不为空,则将当前的 ThreadLocal 作为 key,null 作为 value,插入到 threadLocals。
注意上面的 initialValue()方法为 protected,如果希望线程局部变量具有非 null 的初始值,则必须对 ThreadLocal 进行子类化,并重写此方法。
代码块 5:getEntry 方法
1 2 3 4 5 6 7 8 9 10 11 private Entry getEntry (ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1 ); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
根据 hash code 计算出索引位置
如果该索引位置 Entry 的 key 和传入的 key 相等,则为目标 Entry,直接返回
否则,e 不是目标 Entry,调用 getEntryAfterMiss 方法(见下文代码块 6 详解)继续遍历。
代码块 6:getEntryAfterMiss 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private Entry getEntryAfterMiss (ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null ) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null ) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null ; }
从元素 e 开始向后遍历,如果找到目标 Entry 元素直接返回;如果遇到 key 为 null 的元素,调用 expungeStaleEntry 方法(见下文 expungeStaleEntry 方法详解)进行清除;否则,遍历到 Entry 为 nu
remove()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void remove () { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove(this ); } 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.get() == key) { e.clear(); expungeStaleEntry(i); return ; } } }
方法很简单,拿到当前线程的 threadLocals 属性,如果不为空,则将 key 为当前 ThreadLocal 的键值对移除,并且会调用 expungeStaleEntry 方法清除 key 为空的 Entry。
expungeStaleEntry 方法
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 private int expungeStaleEntry (int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null ; tab[staleSlot] = null ; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; tab[i] = null ; size--; } else { int h = k.threadLocalHashCode & (len - 1 ); if (h != i) { tab[i] = null ; while (tab[h] != null ) h = nextIndex(h, len); tab[h] = e; } } } return i; }
从 staleSlot 开始,清除 key 为 null 的 Entry,并将不为空的元素放到合适的位置,最后遍历到 Entry 为空的元素时,跳出循环返回当前索引位置。
set、get、remove 方法,在遍历的时候如果遇到 key 为 null 的情况,都会调用 expungeStaleEntry 方法来清除 key 为 null 的 Entry。
rehash 方法
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 private void rehash () { expungeStaleEntries(); if (size >= threshold - threshold / 4 ) resize(); } private void resize () { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; Entry[] newTab = new Entry[newLen]; int count = 0 ; for (int j = 0 ; j < oldLen; ++j) { Entry e = oldTab[j]; 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; }
调用 expungeStaleEntries 方法(该方法和 expungeStaleEntry 类似,只是把搜索范围扩大到整个表)清理 key 为空的 Entry
如果清理后 size 超过阈值的 3/4,则进行扩容。
新表长度为老表 2 倍,创建新表。
遍历老表所有元素,如果 key 为 null,将 value 清空;否则通过 hash code 计算新表的索引位置 h,如果 h 已经有元素,则调用 nextIndex 方法直到寻找到空位置,将元素放在新表的对应位置。
设置新表扩容的阈值、更新 size、table 指向新表。
内存泄漏问题
1 2 3 4 5 6 7 8 9 static class Entry extends WeakReference <ThreadLocal <?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } }
从上面源码可以看出,ThreadLocalMap 使用 ThreadLocal 的弱引用作为 Entry 的 key,如果一个 ThreadLocal 没有外部强引用来引用它,下一次系统 GC 时,这个 ThreadLocal 必然会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。
我们上面介绍的 get、set、remove 等方法中,都会对 key 为 null 的 Entry 进行清除(expungeStaleEntry 方法,将 Entry 的 value 清空,等下一次垃圾回收时,这些 Entry 将会被彻底回收)。
但是如果当前线程一直在运行,并且一直不执行 get、set、remove 方法,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value,导致这些 key 为 null 的 Entry 的 value 永远无法回收,造成内存泄漏。
如何避免内存泄漏
为了避免这种情况,我们可以在使用完 ThreadLocal 后,手动调用 remove 方法,以避免出现内存泄漏。