咱们知道ThreadLocal用于维护多个线程线程独立的变量副本,这些变量只在线程内共享,可跨方法、类等,以下是一个维护多个线程Integer变量的ThreadLocal:数组
ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();
每一个使用threadLocalNum
的线程,能够经过形如threadLocalNum.set(1)
的方式建立了一个独立使用的Integer
变量副本,那么它是怎么实现的呢?咱们今天就来简单的分析一下。安全
先看下ThreadLocal的set方法是如何实现的,源码以下:this
public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程 ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap if (map != null) map.set(this, value); //当前线程的ThreadLocalMap不为空则直接设值 else createMap(t, value); //当前线程的ThreadLocalMap为空则建立一个来设置值 }
是的,你没有看错,是获取当前线程中的ThreadLocalMap
来设置的值,咱们来看一下getMap(t)
是如何实现的:线程
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
而后咱们看到Thread中包含了一个ThreadLocalMap类型的属性:设计
ThreadLocal.ThreadLocalMap threadLocals = null;
到这里咱们能够得出一个结论:各个线程持有了一个ThreadLocalMap的属性,经过ThreadLocal设置变量时,直接设置到了对应线程的的ThreadLocalMap属性中。code
那么不一样的线程中经过ThreadLocal设置的值是如何关联定义的ThreadLocal变量和Thread中的ThreadLocalMap的呢?咱们接着分析。对象
前面写到当前线程的ThreadLocalMap为空则建立一个ThreadLocalMap来设值,咱们来看下createMap(t, value)
的具体实现:blog
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } /////////////////// //ThreadLocalMap构造器定义以下 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); } private static final int INITIAL_CAPACITY = 16;
线程中threadLocals是一个ThreadLocalMap变量,其默认值是null,该线程在首次使用threadLocal对象调用set的时候经过createMap(Thread t, T firstValue)
实例化。继承
先来看一下ThreadLocalMap,它是在ThreadLocal中定义的一个静态内部类,其内属性以下:
/** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0
其中属性private Entry[] table
,用于存储经过threadLocal set 进来的变量,Entry定义以下:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry
继承了WeakReference<ThreadLocal<?>>
,ThreadLocal在构造器中被指定为弱引用super(k)
(后面会单独讨论为什么这里使用弱引用)。
至此,咱们能够知道ThreadLocal和Thead的内存结构以下:
网上看到不少文章都在讲ThreadLocal的内存泄露问题,因此也在这里简单说一下本身的理解。
从上面的结构能够看出ThreadLocal涉及到的要回收的对象包括:
下面先简述java的引用,而后分别讨论ThreadLocal自己的回收和threadLcoalMap的回收
ThreadLocal实例的引用主要包括两种:
强引用还在的状况下ThreadLocal必定不会被回收;无强引用后,因为各个Thread中Entry的key是弱引用,会在下次GC后变为null。ThreadLocal实例何时被回收彻底取决于强引用什么时候被干掉,那么何时强引用会被销毁呢?最简单的就是 threadLocal=null
强引用被赋值为null;其它也但是threadLocal是一个局部变量,在方法退出后引用被销毁,等等。
这里来回答一下前面提到的为何ThreadLocalMap中将key设计为弱引用,咱们假设若是ThreadLocalMap中是强引用会出现什么状况?定义ThreadLocal时定义的强引用被置为null的时候,若是还有其它使用了该ThreadLocal的线程没有完成,还须要好久会执行完成,那么这个线程将一直持有该ThreadLocal实例的引用,直到线程完成,期间ThreadLocal实例都不能被回收,最重要的是若是不了解ThreadLocal内部实现,你可能都不知道还有其余线程引用了threadLocal实例。
线程结束时清除ThreadLocalMap的代码Thread.exit()
以下:
/** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */ private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
单从引用的角度来看,各线程中的threadLocalMap,其中包括各个Entry的key 和 value,线程(也就是Thread实例)自己一直持有threadLocalMap的强引用,只有在线程结束的时候才会被回收,可是ThreadLocal在实现的时候提供了一些方法:set/get/remove,能够在执行它们的时候回收其它已经失效(key=null)的entry实例。
这里就以set为例看看ThreadLocal是如何回收entry的,ThreadLocal set方法实现以下:
//ThreadLocal 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 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); //获取当前threadLocal实例的hashcode,同时也是table的下标 //这里for循环找key,是由于hash冲突会使hashcode指向的下标不是真实的存储位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //找到了设置为新值 if (k == key) { e.value = value; return; } //entry不为null,key为null //说明原来被赋值过,可是原threadLocal已经被回收 if (k == null) { replaceStaleEntry(key, value, i); return; } } //若是下标对应的entry为null, 则新建一个entry tab[i] = new Entry(key, value); int sz = ++size; //清理threadlocal中其它被回收了的entry(也就是key=null的entry) if (!cleanSomeSlots(i, sz) && sz >= threshold) //rehash rehash(); }
看一下cleanSomeSlots的实现:
//ThreadLocalMap private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //获取下一个entry的下标 i = nextIndex(i, len); Entry e = tab[i]; //entry不为null,key为null //说明原来被赋值过,可是原threadLocal已经被回收 if (e != null && e.get() == null) { n = len; removed = true; // 删除已经无效的entry i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 回收无效entry tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //entry不为null,key为null,应该回收 if (k == null) { e.value = null; tab[i] = null; size--; } else { //rehash的实现 //计算当前entry的k的hashcode,看是下标是否应该为i //若是不为i说明,是以前hash冲突放到这儿的,如今须要reash int h = k.threadLocalHashCode & (len - 1); //h!=i 说明hash冲突了, entry不该该放在下标为i的位置 if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. //找正确的位置h,可是仍是有可能冲突因此要循环 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
从上面的分析咱们能够看到把ThreadLocalMap中的key设计为weakReference,也使set方法能够经过key==null && entry != null
判断entry是否失效。
总结一下ThreadLocal set方法的实现:
ThreadLocal经过巧妙的设计最大程度上减小了内存泄露的可能,可是并无彻底消除。
当咱们使用完ThreadLocal后没有调用set/get/remove方法,那么可能会致使失效内存不能及时被回收,致使内存泄露,尤为是在value占用内存较大的状况。
因此最佳实践是,在明确ThreadLocal再也不使用时,手动调用remove方法及时清空。
cleanSomeSlots
方法回收键为 null 的 Entry 对象的值(即失效实例)从而防止内存泄漏(其它的remove,get相似)