赞
踩
让某个需要用到的对象在线程间隔离(每个线程都有自己独享的对象)
任何方法中都可以轻松获取其对象
好处
该方法会返回当前线程对应的初始值,采用了懒加载机制,当第一次get的时候才会触发,当线程第一次使用get方法的时候才会触发。除非线程先前调用了set方法,在这种情况下,不会再调用InitValue方法
未当前线程设置一个新的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回对象
}
}
return setInitialValue();//如果第一次调用get,ThreadLocalMap未空或者在ThreadLocalMap中还未存储对象,则进行初始化并返回存储对象
}
//移除线程所存储对象
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
在Thread类中又这样一个ThreadLocalMap 类型成员变量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是ThreadLocal的内部类,其结构如HashMap很相似,在其内部还有个Entry,保存ThreadLocal和其保存的对象。其默认容量也为16,负载因子未2/3,并且不存在next指针,哈希冲突后采用的延后策略。具体请看最后问题栏
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 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; //负载因子是2/3, } //......省略............ }
总的来说,在Thead中维护了一个Map,在Map中存储了ThreadLocal和其绑定的对象
每次获取对象都会从当前线程中获取map并将ThreadLocal传入从而获得对象
ThreadLocal被用作TheadLocalMap的弱引用key,这种设计也是ThreadLocal被讨论内存泄露的热点问题,因此有必要了解一下什么是弱引用。
弱引用是用来描述非必须的对象的,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。
由于ThreadLocalMap是以弱引用的方式引用着ThreadLocal,换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用,则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组KV的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存。
综上,发生内存泄露的条件是
class Test{ byte data[]=new byte[1024*1024*10]; @Override protected void finalize() throws Throwable { System.out.println("destroy"); } } public class ThreadLocalDemo { public ThreadLocal<Test> t = new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); Test test = new Test(); threadLocalDemo.t.set(test); test = null; //threadLocalDemo.t.remove(); threadLocalDemo = null; System.out.println("start gc"); System.gc(); try { Thread.sleep(1000L); }catch (Exception e) { e.printStackTrace(); } System.out.println("end"); } } //输出 /* start gc end */ //当threadLocalDemo.t.remove();不被注释 /* 输出: start gc destroy end */
当不在持有ThreadLocalDemo对象,因为thread中ThreadLoaclMap中保存有ThreadLocal的引用 ,如果ThreadLocal不是弱引用的话,ThreadLocal是不可能被gc的。而如果ThreadLocal与ThreadLocalMap之间是弱引用,如果除Thread外没有任何对象可以获得ThreadLocal,则ThreadLocal是可以为回收的
当然,其仍然仍然存在一定的内存泄露,即value与TreadLcoalMap之间存在引用,当ThreadLocal被gc时value是无法被gc的,但是在ThreadLocalMap内部也存在一些机制,当map扩容或者发生hash冲突的时候会判断key键是否为null(即判断ThreadLocal对象是否被回收),如果是null,则会将value值同样设为Null.从而帮助value gc
public class ThreadLocalDemo2 { public ThreadLocal<Test> t = new ThreadLocal<>(); //public static ThreadLocal<Test> t = new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalDemo2 threadLocalDemo = new ThreadLocalDemo2(); Test test = new Test(); test.name = "xxxx"; threadLocalDemo.t.set(test); ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2(); Test test2 = new Test(); test2.name = "yyyyy"; threadLocalDemo2.t.set(test2); System.out.println(threadLocalDemo.t.get().name); System.out.println(threadLocalDemo2.t.get().name); } } /* 输出: xxxx yyyyy static 修饰 ThreadLocal 输出: yyyyy yyyyy */
static修饰ThreadLocal后,单个线程无论创建多个对象,其ThreadLocal示例仅仅只有一个。
如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源,甚至会造成内存泄漏.。
通过前面几节的分析,我们基本弄清楚了ThreadLocal相关设计和内存模型,对于是否会发生内存泄露做了分析,下面总结下几点建议:
参考:https://www.jianshu.com/p/1a5d288bdaee
为什么ThreadLocalMap不用HashMap而是自己写了个Map
ThreadLocalMap达到扩容的阈值时会真正的扩容吗?
不会,达到阈值之后,进行一个散列表的扫描清楚过期的数据,如果清理完之后,数据量仍然达到其阈值的75%,才进行扩容
扩容源码:
private void rehash() { expungeStaleEntries();//清理 if (size >= threshold - threshold / 4)//数据量仍然达到其阈值的75%,才进行扩容 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; //将value设为null从而帮助GC } else { //重新进行hash int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen);//采用的时自定义hash算法 newTab[h] = e; count++; } } } setThreshold(newLen);//计算新的阈值 size = count; table = newTab; }
ThreadLocalMap获取Entry的流程
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1);//hash运算计算出位置 Entry e = table[i]; if (e != null && e.get() == key)//未发生过Hash冲突 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) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null)//如果为空,则说明此位置被GC了,为过期数据 expungeStaleEntry(i);//为了防止内存泄漏,触发一个“探测式”过期数据回收逻辑 else i = nextIndex(i, len);//计算下一个位置 e = tab[i]; } return null; } //“探测式”过期数据回收逻辑 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null;//将value设为空,帮助GC tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot, len);//根据hash和寻址算法遍历所有与当前hash相同的槽点 (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) {//帮助GC e.value = null; tab[i] = null; size--; } else { //如果key不为空,则重新进行hash,将其移动到一个更靠近其hash位置的槽点(提高下次get的效率) 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; }
ThreadLocalMap中set的具体流程
private void set(ThreadLocal<?> key, Object value) { //寻址 Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //遍历可能的slot for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //如果key相同,则替换 if (k == key) { e.value = value; return; } //如果k为空,则进行取代算法 if (k == null) { //大体就是遍历可能的槽点,直到碰到key值相同的,则将其移动到距离真实hash位置最近的点,如果没有,则再最有好的位置new一个新的Entry replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size;//判断是否达到扩容条件 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。