各位读者大家好呀!今天小卡给大家带来的是小卡面试过程中被面试官问到的一个问题~
ThreadLocal介绍
ThreadLocal 是 Java 中的一个类,用于创建线程局部变量。线程局部变量的作用范围仅限于单个线程,每个线程都有自己的独立副本,互不影响。这对于需要在线程之间隔离状态的多线程编程非常有用,可以避免共享变量导致的线程安全问题。
主要特点
- 线程隔离:
-
- 每个线程都有一个独立的
ThreadLocal变量副本,这些副本之间不会互相干扰。 - 这使得
ThreadLocal成为实现线程安全的一种简单方法,尤其是在处理复杂的多线程应用程序时。
- 每个线程都有一个独立的
- 生命周期:
-
ThreadLocal变量的生命周期与线程的生命周期绑定。当线程结束时,该线程中的所有ThreadLocal变量都会被自动清除。- 如果线程是长时间运行的(如线程池中的线程),则需要注意手动清理
ThreadLocal变量,以避免内存泄漏。
- 初始化:
-
- 可以通过
initialValue方法为ThreadLocal变量提供初始值。 - 如果没有显式地设置初始值,那么默认值为
null。
- 可以通过
基本用法
以下是一个简单的 ThreadLocal 示例:
public class ThreadLocalExample {
// 定义一个ThreadLocal变量
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 在主线程中设置ThreadLocal变量的值
threadLocal.set("Main Thread Value");
// 打印主线程中的ThreadLocal变量值
System.out.println("Main Thread: " + threadLocal.get());
// 创建并启动一个新的线程
Thread t1 = new Thread(() -> {
// 在新线程中设置ThreadLocal变量的值
threadLocal.set("Thread 1 Value");
// 打印新线程中的ThreadLocal变量值
System.out.println("Thread 1: " + threadLocal.get());
});
t1.start();
try {
t1.join(); // 等待t1线程执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次打印主线程中的ThreadLocal变量值
System.out.println("Main Thread after t1: " + threadLocal.get());
}
}
ThreadLocal是怎么做到线程隔离的?
如果要让某一个变量,具有线程隔离性,一种方法就是让该变量成为线程的私有成员变量,但是该怎么把这个变量放进去呢?
public class RunnableDemo<T> implements Runnable{
private T value;
public RunnableDemo(T value){
this.value = value;
}
@Override
public void run() {
Thread.currentThread().setValue(value);
}
}
假设Thread中有这样一个变量专门用于线程隔离的,那么其实我们就可以在run()方法中为这个变量赋值,但是如果只是一个变量局限性就太大了,因为可能不只有一个需要线程隔离的变量,所以最好呀在Thread中有一个Map用于存储需要线程隔离的变量而,ThreadLocal其实也就是这么干的,ThreadLocal中有这样一个静态内部类ThreadLocalMap。
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
而在Thread类内部就持有该Map,也就是说其实我们在调用threadLocal.set()方法设置ThreadLocal变量的值的时候大概率其实是把值存入到了,当前线程所持有的TheadLocalMap中了。那这个变量到底是怎么存储的呢?
ThreadLocal中的一些关键方法
/**
* 设置当前线程的 ThreadLocal 变量的值。
*
* @param value 要设置的值
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果 map 不为空,则在 map 中设置当前线程的 ThreadLocal 变量的值
if (map != null) {
map.set(this, value);
} else {
// 如果 map 为空,则创建一个新的 ThreadLocalMap 并设置值
createMap(t, value);
}
}
/**
* 创建一个新的 ThreadLocalMap 并将其关联到当前线程。
*
* @param t 当前线程
* @param firstValue 初始值
*/
void createMap(Thread t, T firstValue) {
// 将新的 ThreadLocalMap 关联到当前线程的 threadLocals 字段
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 获取当前线程的 ThreadLocal 变量的值。
*
* @return 当前线程的 ThreadLocal 变量的值
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果 map 不为空,则从 map 中获取当前线程的 ThreadLocal 变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 强制类型转换,获取存储的值
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 如果 map 为空或未找到对应的 Entry,则返回初始值
return setInitialValue();
}
/**
* 获取指定线程的 ThreadLocalMap。
*
* @param t 指定的线程
* @return 指定线程的 ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从这两组方法的源码中可以知道,如果在一个线程中第一次调用threadLocal.set()方法,threadLocal就会为当前线程初始化一个theadLocaLMap,然后将真正的线程隔离变量存储到threadLocalMap中。
ThreadLocal为什么会存在内存泄漏问题?
ThreadLocalMap的结构
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
......
}
ThreadLocalMap中维护的是一个Entry[]类型节点数组,一个键值对节点就是一个Entry对象。我们会发现所有的Entry节点key值都是ThreadLocal类型,原理每一个ThreadLocal变量值在ThreadLocalMap中都是以当前threadLocal对象为键,ThreadLocal变量值为值,的键值对形式存储的。并且Entry对象是实继承了WeakReference弱引用类,并且可以看到,在Entry中对key(ThreadLocal)的引用是弱引用,也就说当外界不在对TheadLocal进行引用是(也就说外部不在使用当前threadLocal变量时)那么,GC就会考虑去回收当前threadLocal,
为什么要把Entry中对ThreadLocal引用设置为弱引用呢?
讲到这里我们还需要了解ThreadLocalMap的惰性删除策略,才能真正搞明白为什么要把Entry中的TheadLocal引用设置为弱引用。
ThreadLocalMap处理hash冲突的方法
ThreadLocalMap在遇到hash冲突时使用的使用的是开放地址法(线性探测法),如下图
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table的长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。按照上面的描述,可以把Entry[] table看成一个环形数组。
ThreadLocalMap惰性删除策略
所谓的ThreadLocalMap惰性删除策略就是,在调用set()、get()、rehash()等方法遍历数组时发现key值为空的元素就顺带对其进行清楚。下面笔者将举例讲解set()方法的惰性删除。
/**
* 返回下一个索引,如果当前索引是最后一个,则返回 0。
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private void set(ThreadLocal<?> key, Object value) {
// 获取当前线程的 ThreadLocalMap
Entry[] tab = table;
// 获取数组的长度
int len = tab.length;
// 计算 key 的哈希码,并找到对应的索引
int i = key.threadLocalHashCode & (len - 1);
// 遍历数组,查找与 key 相匹配的 Entry,如果存在的话就进行替换。
//其实整个线性探测过程是这样的
//首先探测的索引位置应该就是通过hash值与hash表掩码计算出来的索引下标。
//这时候就会存在几种可能1.当前索引下标元素为空的话,就会跳出循环
//2.当前索引下标元素不为空,并且k==key,那么直接就替换节点中的value值
//3.当前索引下标元素不为空,但是k==null,那么此时我们不能判断数组中是否存在
//与key相匹配的Entry,所以需要借助replaceStaleEntry(key, value, i);继续探寻。
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取当前 Entry 的 key
ThreadLocal<?> k = e.get();
// 如果找到了匹配的 key,则更新其值
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果没有找到匹配的 key,则创建一个新的 Entry 并插入到数组中
tab[i] = new Entry(key, value);
// 增加 size 计数器
int sz = ++size;
// 尝试清理一些 stale entry,并检查是否需要重新调整数组大小
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
// 获取当前线程的 ThreadLocalMap
Entry[] tab = table;
// 获取数组的长度
int len = tab.length;
Entry e;
// 从 staleSlot 开始向前查找,找到第一个 stale entry(即 key 为 null 的 entry)
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)) {
if (e.get() == null) {
slotToExpunge = i;
}
}
// 从 staleSlot 开始向后查找,找到 key 或者遇到第一个 null slot
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到了匹配的 key,则交换它与 stale entry 的位置
if (k == key) {
e.value = value;
// 交换两个 entry 的位置
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果 slotToExpunge 仍然是 staleSlot,更新为当前的 i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理 stale entry 并可能重新调整数组大小
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果没有在向前扫描时找到 stale entry,并且当前 entry 为 null,则更新 slotToExpunge
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果没有找到匹配的 key,则在 staleSlot 创建一个新的 entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果在当前 run 中还有其他 stale entry,清理它们
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
所以其实需要被惰性删除的元素应该是那些,key==null,之所以吧Entry中对TheadLocalMap中的的引用设置为弱引用就是为了传递一个信息,那就是外界已经不在使用该ThreadLocal变量了,ThreadLocalMap中存储的键值对可以被清除。
既然有了惰性删除策略ThreadLocal为什还是存在内存泄露问题呢?
其实这个问题,就要结合实际的应用,在大部分的应用场景中,特别是并发编程中,其实我们的线程和任务都是交由线程池进行管理的,以便于线程的复用,避免频繁创建线程找造成的性能浪费。那么线程就会被长期的驻留在线程池中不会被回收,那么Thread中引用的TheadLocalMap也就不会被主动回收,那么一但某个线程不在使用TheadLocal变量,也就不在会调用ThreadLocalMap中的set()、get()等方法,那么惰性删除策略也就失效了,那么如果我们在使用完ThreadLocal变量之后,没有调用remove()方法及时的把ThreadLocalMap中的键值对删除,并且惰性删除策略也没有及时的吧ThreadLocalMap中的键值对清空那么,这些键值对就会随着Thread的存活一直存在,导致大量占用内存,导致内存泄露问题,所以我们在使用ThreadLocal变量时一定要养成良好的习惯,在使用完ThreadLcoal变量后一定要调用remove()方法,删除ThreadLocalMap中不再使用的键值对。
好的本期内容就讲到这里,如果读者们在阅读中有什么不同地意见欢迎到评论区进行指正哈~