ThreadLocal原理探究
四大引用是什么,分别有什么特点:
1 强引用、软引用、弱引用、虚引用
强引用:发生gc的时候,只要对象还有引用,就不会被回收
软引用:发生gc的时候,内存够用就不会回收,内存不够时,就会回收。可以及时的避免oom。
Map<String,SoftReference<BitMap>> imageCache = new HashMap<String,SoftReference<BitMap>>();
弱引用:发生gc的时候,马上就会回收。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾收集器回收。它不能单独使用也不通过通过它访问对象。虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的目的是跟踪对象被垃圾回收的状态。仅仅是提供了确保对象被finalize之后做某些事情的机制。PhantomReference的get方法总是返回null.
四种引用垃圾回收的场景:
思考:
1 ThreadLocal中的ThreadLocalMap的数据结构与关系?
Thread中有一个成员变量ThreadLocalMap。ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap中有一个静态内部类Entry. Entry是对ThreadLocal的弱引用。
2 Entry的key是弱引用,为什么?
每个Thread对象维护着ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry进行存储。
调用ThreadLocal的set方法时,实际是往ThreadLocalMap设置值,key是ThreadLocal对象, value是传进来的对象。
调用ThreadLocal的get方法时, 实际是从ThreadLocalMap中获取值。
ThreadLocal本身并不存储值,它只是以自己作为key从线程的ThreadLocalMap获取value, 正因为如此,ThreadLocal实现了线程隔离,获取当前线程的局部变量值,不受其他线程影响。
ThreadLocal提供线程局部变量,每一个线程在访问ThreadLocal实例的时候(通过其get与set方法)都有自己的,独立的初始化变量副本。Threadlocal实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来。
//ThreadLocal的方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); // 使用this引用作为key,既做到了变量相关,又满足key不可变的要求。 else createMap(t, value); }
//ThreadLocalMap的方法
private void set(ThreadLocal<?> key, Object value) { // map中就是使用Entry[]保留所有的entry实例 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; } } //不存在就新建一个entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
//ThreadLocal的方法
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(); }
//ThreadLocalMap的方法
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); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } }
想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到了当前线程中的一个Map中去了。
ThreadLocal内存泄露问题?
虚拟机栈栈中的ThreadLocal Ref 与堆上的ThreadLocal是强引用的关系,当虚拟机栈中的ThreadLocal Ref 用完之后,准备被回收时,如果entry与堆上的ThreadLocal不是弱引用的话,那么堆上的ThreadLocal对象就不会被回收。而如果是弱引用,当发生Gc时,ThreadLocal用完之后,不存在强引用,而唯一存在一个弱引用,此时ThreadLocal对象就可以被回收了。虽然弱引用可以让堆上的ThreadLocal对象被回收,但是Entry对象还因为线程的存在,有引用可达。ThreadLocal被回收之后,entry里面的key是null, 此时这个entry成为了陈旧项,value对象在线程存在时,仍然会存在,故而仍然有线程泄露的风险。因此就需要某种机制,来对Entry里面的key为null的value进行释放。ThreadLocal采用了线性探测来清除陈旧项(replaceStaleEntry与getEntryAfterMiss方法都干了这个活),从而防止了内存泄漏。当然,我们也可以在用完ThreadLocal之后,手动调用remove方法,去除线程中ThreadLocalMap中的这个entry.
4 ThreadLocal中为什么要加remove方法?
主要是为了及时地将线程中TreadLocalMap中的以这个ThreadLocal为key的entry中的value对象删除。这样就可以避免因为线程长期存在,而ThreadLocal实例用完之后导致的内存泄露问题。
总结:
ThreadLocal并不解决线程间共享数据的问题,而是为每个线程提供了一个独立的变量副本。从而避免线程变量的线程安全问题。由于每个线程都有一个执属于自己的ThreadLocalMap,并维护了ThreadLocal与实例之间的映射关系,因此就不存在线程安全以及锁的问题。ThreadLocalMap的entry对ThreadLocal弱引用,避免了ThreadLocal无法被回收的问题。ThreadLocal自身的set、get、remove方法最终会调用expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键位null的entry对象的value值(实例),以及entry对象本身,从而防止内存泄露。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)