前言
本文将讲述ThreadLocal的实现原理,还有## ThreadLocal为什么使用弱引用。
ThreadLocal
ThreadLocal 是 Java 中的一个类,用于在多线程环境下为每个线程提供独立的变量副本。它通常用于解决多线程并发访问共享变量时的线程安全性问题。
ThreadLocal 的工作原理是每个线程内部维护一个 ThreadLocalMap 对象,该对象用于存储每个线程的变量副本。当通过 ThreadLocal 对象获取变量时,它会首先检查当前线程是否已经创建了该变量的副本,如果有,则直接返回副本;如果没有,则通过初始化方法创建一个新的副本,并将其保存在当前线程的 ThreadLocalMap 中。
使用 ThreadLocal 时,每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。这使得在多线程环境中共享变量变得更加安全和可靠。
需要注意的是,使用 ThreadLocal 时要注意及时清理不再使用的变量副本,以避免内存泄漏问题。可以通过调用 remove()
方法来清除当前线程的变量副本。
源码解释
set方法源码
// ThreadLocal的set方法,value是要保存的值
public void set(T value) {
// 得到当前线程对象
Thread t = Thread.currentThread();
// 得到当前线程对象关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 得到map对象就保存值,键为当前ThreadLocal对象
// 如果没有map对象就创建一个map对象,保存值
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 得到当前线程关联的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建一个ThreadLocalMap对象,赋给当前线程的threadLocals属性,并且存入值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 通过key计算在tab数组中的槽位i
int i = key.threadLocalHashCode & (len-1);
// 拿到槽位上的Entry对象,如果不为null,则进入循环,如果为null则表示可以直接加入该槽位
// e = tab[i = nextIndex(i, len)])取出下一个槽位的Entry实体,如果为null,则表示可以直接添加进该槽位
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 拿到与当前Entry有关联的ThreadLocal对象
ThreadLocal k = e.get();
// 如果k与当前要保存值的key相等,则替换掉value,相当于修改key的值
if (k == key) {
e.value = value;
return;
}
// 检查当前节点的ThreadLocal如果为null,表示ThreadLocal已经被gc回收,则调用 replaceStaleEntry() 方法来替换陈旧的 Entry,将新的 ThreadLocal 和值插入到数组中的索引位置 i 处,并返回。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 创建一个Entry对象,加入i槽位
tab[i] = new Entry(key, value);
// 记录Entry对象个数
int sz = ++size;
// cleanSomeSlots清理陈旧的Entry,清理完后如果大于阈值,则调用rehash扩容数组
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法源码
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 得到当前线程关联的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通过key获取到Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// Entry不为空,则直接获取值返回结果
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map为null,或者Entry为null,则返回一个初始化值
return setInitialValue();
}
private T setInitialValue() {
// 如果是在调用构造器初始化的ThreadLocal对象,该方法直接返回null
// 如果是调用的静态方法withInitial,则返回你指定的一个初始化则
// 并且还会把该初始化的值保存进ThreadLocalMap
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public static ThreadLocal withInitial(Supplier key) {
// 计算当前key的落脚点
int i = key.threadLocalHashCode & (table.length - 1);
// 取出落脚点的Entry对象
Entry e = table[i];
// 如果e不为空,并且跟e关联的ThreadLocal对象等于当前的key,则返回当前e对象
if (e != null && e.get() == key)
return e;
// 否则进入getEntryAfterMiss
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 如果e为null,则直接返回null,表示当前key并没有数据
while (e != null) {
// 取出与e关联的ThreadLocal对象
ThreadLocal k = e.get();
// 判断k是否等于当前的ThreadLocal对象
if (k == key)
return e;
// 当前k是否等于null,为null表示被gc垃圾回收,就清理旧的Entry对象
if (k == null)
expungeStaleEntry(i);
else
// 否则k不为null,取出下一个槽位,接着循环
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
三者关系
总结
可以看出实际保存线程局部变量的是ThreadLocalMap对象,每个线程都有一个这样的对象,保存的是键值对,键为当前的ThreadLocal对象,ThreadLocal对象一般设置为静态,非静态只会造成对象的冗余,因为ThreadLocalMap的键只能是当前ThreadLocal对象,所以只能保存一个键值对,如果要保存多个键值对,可以定义多个ThreadLocal对象作为不同的键,这样获取到的还是与线程有关联的ThreadLocalMap对象,而ThreadLocalMap的键是当前的ThreadLocal对象,多少个该对象,那就可以保存多少个值。
强软弱虚四大引用
在Java中,引用是用于引用对象的一个机制,它允许我们通过引用变量来操作和访问对象。在Java中,主要有以下几种引用类型:
new
关键字创建对象时,默认就是使用强引用。如果一个对象具有强引用,即存在一个强引用变量引用它,那么垃圾回收器就不会回收该对象。只有当对象没有任何强引用时,才会被认为是不再需要的,可以被垃圾回收。SoftReference
类来创建软引用。WeakReference
类来创建弱引用。在内存管理方面,软引用和弱引用都可以用于解决一些特定的内存问题,例如缓存管理或对象关联。它们对于临时性或可替代性对象的管理非常有用,可以在内存紧张时进行垃圾回收,从而提高系统的性能和可用性。然而,需要注意的是,对于软引用和弱引用对象,程序应该在使用时进行必要的判空和恢复处理,以避免 NullPointerException 和其他相关问题。
ThreadLocal为什么使用弱引用
ThreadLocal 使用弱引用的主要原因是为了避免内存泄漏问题。
当使用强引用持有 ThreadLocal 对象时,只有线程销毁或显式地调用 remove()
方法时,Entry 才会被释放。这可能导致在多线程环境下使用线程池时,即使线程已经使用结束处于空闲状态,对应的 Entry 仍然会存在于 ThreadLocalMap 中,导致无法回收相关资源,从而造成内存泄漏。
使用弱引用作为 ThreadLocal 的键(key),可以解决这个问题。弱引用在垃圾回收时只要发现只有弱引用指向,则会被直接回收。因此,当线程结束且对应的 ThreadLocal 对象只有弱引用存在时,垃圾回收器会自动清理该弱引用,进而清理 ThreadLocalMap 中对应的 Entry。这样可以避免内存泄漏问题。
需要注意的是,尽管 ThreadLocalMap 使用了弱引用来避免内存泄漏问题,但仍然需要在使用 ThreadLocal 后调用 remove()
方法,以确保及时清理 ThreadLocal 对象和对应的值。这是因为弱引用的回收时机不确定,不能完全依赖垃圾回收器的工作。
当我们应该请求进来分配一个线程处理请求,此时ThreadLocal对象就会被创建,并且是一个强引用,当第一次把值存入时ThreadLocal时,就会通过Thread拿到或者创建一个ThreadLocalMap对象,并且存入我们的数据,此时ThreadLocal作为键就会被放入弱引用中,此时就算发送垃圾回收也不会回收ThreadLocal因为有一个强引用指向,但是一旦我们的请求执行完毕返回,线程处于空闲状态时,这个强引用就没了,此时就剩下一个弱引用,这个时候发生垃圾回收就ThreadLocal就会被收回。