同一个threadlocal变量所包含的对象,在不一样的线程中是不一样的副本。数组
既然是不一样的线程拥有不一样的副本且不容许其余线程访问,因此不存在共享变量的问题。安全
解决的是变量在线程间隔离在方法或类之间共享的问题。函数
解决这个问题可能有的两种方案this
第一种:一个threadlocal对应一个map,map中以thread为keyspa
这种方法多个线程针对同一个threadlocal1是同一个map,新增线程或减小线程都须要改动map,这个map就变成了多个线程之间的共享变量,须要额外机制好比锁保证map的线程安全。线程
线程结束时,须要保证它所访问的全部 ThreadLocal 中对应的映射均删除,不然可能会引发内存泄漏。-----为何会致使内存泄漏呢?见下文code
第二种:一个thread对应一个map,map中以threadlocal为key区分对象
Thread中有一个变量blog
ThreadLocal.ThreadLocalMap threadLocals = null;//每一个线程维护一个map
ThreadLocalMap索引
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } private Entry[] table;
ThreadLocalMap维护了一个table数组,存储Entry类型对象,Entry类型对象以ThreadLocal为key,任意对象为值的健值对
ThreadLocal的get set
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);//返回当前线程维护的threadlocals变量 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } 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; }
Entry key的弱引用以及内存泄漏
为何说threadlocal会存在内存泄漏:
每一个thread维护的threadlocalmap key是指向threadlocal的弱引用,当没有任何其余强引用指向threadlocal的时候,gc会把key回收。
但value是是thread指向的强引用,thread不结束,value不会被回收。
因此当threadlocal不可用但thread还在的这段时间内,会存在所说的内存泄漏。尤为当使用线程池的时候,线程被复用。
jdk有没有相应的处理:
再回到threadlocalmap的get set方法
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //获取当前线程的map if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //找到当前的threadlocal if (e != null) return (T)e.value;//取值 } return setInitialValue();// map为null或者map找不到指定key时,初始化基本值,不展开 } //getEntry函数 private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1);//根据key计算索引 Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e);//table中该索引位置对象e为null 或者 索引位置key不符进入getEntryAfterMiss } //getEntryAfterMiss函数 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //遍历table一直到找到了k=key的位置,返回相应对象e //遍历过程当中若是遇到了k为null,即调用expungeStaleEntry清理该entry,即前面所说的内存泄漏,这里是处理的一个时机 while (e != null) { ThreadLocal k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); //循环遍历table,ThreadLocal采用的是开放地址法,即有冲突后,把要插入的元素放在要插入的位置后面为null的地方 e = tab[i]; } return null;//若是是e为null 返回null } //expungeStaleEntry 函数 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot:key为null的索引位置的对象.value置为null,对象也置为null 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; //遍历是从staleSlot以后到遇到的第一个e为null i = nextIndex(i, len)) { ThreadLocal k = e.get(); if (k == null) {//遍历的过程当中遇到key为null作和上面一样的处理 e.value = null; tab[i] = null; size--; } else { //key不为null的从新hash int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } //再看set public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } //重点在map.set函数 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)]) { //若是根据索引找到的entry不是空的 ThreadLocal k = e.get(); if (k == key) { //key相同,value直接覆盖 e.value = value; return; } if (k == null) { //遍历过程当中key为null,清除 replaceStaleEntry(key, value, i); return; } } //上面没有处理掉,找到第一个为null的能用的坑位,new一个entry放入 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
能大体看到,上面的代码中,threadlocalmap的get set都会作对key为null的清除工做,从而解决了上面说的内存泄漏问题,只是这种处理依赖对set get的调用
threadlocalmap的remove方法:
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; } } }
综上所述,不少地方会看到有这样的两条建议:
1 .使用者须要手动调用remove函数,删除再也不使用的ThreadLocal.
2 .还有尽可能将ThreadLocal设置成private static的,这样ThreadLocal会尽可能和线程自己一块儿消亡。