什么是ThreadLocal
ThreadLocal是Java中的一个类,它提供了一种将对象与线程关联的机制。每个线程都可以通过ThreadLocal获取自己的独立副本,以保证线程间的数据隔离和线程安全。
ThreadLocal的作用主要有以下几个方面:
- 线程隔离:每个线程可以独立地操作自己的副本,互不干扰。
- 线程封闭:通过ThreadLocal,可以将可变的数据封装在每个线程内部,使其具有线程级别的封闭性。
- 上下文传递:ThreadLocal常被用于存储线程相关的上下文信息,如用户身份、请求参数等。
- 避免传参:有些情况下,某些数据需要在多个方法之间传递,使用ThreadLocal可以避免频繁的参数传递。
ThreadLocal是如何工作的?
ThreadLocal核心逻辑都是在Thread类的静态内部类ThreadLocalMap中,接下来一起分析下ThreadLocalMap源码,逐个方法解析:
ThreadLocalMap类中有个静态内部类Entry,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随时可能消失,也就是entry.get()返回的ThreadLocal随时可能为空。
//这个线程的关于所有的ThreadLocal保存的数据都在这,通过ThreadLocal确定下坐标找到对应的entry
private Entry[] table;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过threadLocal确定下坐标找到对应的entry
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;
}
//这里!!!如果e不是null,但是e.get()是null,表示那个弱引用被垃圾回收了
if (k == null) {
//这个方法会删除所有过期的条目
//意思就是将被垃圾回收掉的threadLocal,从数据table中删除
//并且新new 一个entry代替掉它的i坐标,过分!
replaceStaleEntry(key, value, i);
return;
}
}
//如果没有直接新增一个到数组里边
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通过上边的set方法,可以发现ThreadLocalMap真正存储数据的地方是table数组里边,通过threadLocal的hash值找下坐标。如果在set的时候发现table中threadLocal被回收掉了,那么它的位置直接被顶替掉。
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);
}
这个get方法没什么好说的,就是用threadLocal的hash值去找table的下坐标,没有找到直接循环数组找。好了ThreadLocalMap的核心逻辑就分析到这。
接下来看ThreadLocal核心源码
public void set(T value) {
// 拿到当前线程
Thread t = Thread.currentThread();
//获取当前线程下的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
// 拿到当前线程
Thread t = Thread.currentThread();
//获取当前线程下的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过threadLocal获取table里边的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue();
}
threadLocal和ThreadMap的关系
图解释:
建了三个ThreadLocal,有两个线程,在线程里边ThreadLocalMap类的属性table分别保存着threadLocal对应的value值。
ThreadLocal内存泄漏问题
内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。通常少次数的内存无法及时回收并不会到程序造成什么影响,但是如果在内存本身就比较少获取多次导致内存无法正常回收时,就会导致内存不够用,最终导致内存溢出。
首先「说一下为什么在ThreadLocalMap中ThreadLocal是弱引用?」
如果在ThreadLocalMap中使用强引用来存储ThreadLocal对象,那么ThreadLocal对象会一直存在于内存中,即使在实际的应用中已经不再需要该ThreadLocal对象。这是因为ThreadLocalMap是与线程相关联的,保存在ThreadLocalMap的table数组中,ThreadLocalMap中的键值对不会被自动清理,而是会一直保留,从而造成内存泄漏。也就是说ThreadLocal永远不会被清理,除非手动清理,ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
为了解决这个问题,ThreadLocal被存储为弱引用。弱引用的特点是,当一个对象只被弱引用所引用时,在垃圾回收时会被自动回收。当ThreadLocal对象被垃圾回收时,对应的键值对也会被自动从ThreadLocalMap中移除。
「threadLocal内存泄漏问题」
如果一个线程长时间运行,一直持有ThreadLocal对象的键值,由于value被强制引用,没有及时清理导致内存泄漏。
为了避免ThreadLocal内存泄漏问题,需要进行适当的清理操作。可以使用ThreadLocal类提供的remove()方法手动清理ThreadLocal对象关联的值。