1. 散列表基本原理
在介绍ThreadLocal对象之前,先就其底层的实现结构——散列表,即hash Table的原理进行简单说明:
散列表是基于散列思想实现的 Map 数据结构,将散列思想应用到散列表数据结构时,就是通过 hash 函数提取键(Key)的特征值(散列值),再将键值对映射到固定的数组下标中,利用数组支持随机访问的特性,实现 O(1) 时间的存储和查询操作。
在从键值对 映射到数组下标的过程中,散列表会存在 2 次散列冲突:
- 第 1 次 - hash 函数的散列冲突: 这是一般意义上的散列冲突(由算法导致的不同的key计算得到相同的hash值);
- 第 2 次 - 散列值取余转数组下标: 本质上,将散列值转数组下标也是一次 Hash 算法,也会存在散列冲突。
事实上,由于散列表是压缩映射,所以我们无法避免散列冲突,只能保证散列表不会因为散列冲突而失去正确性。常用的散列冲突解决方法有 2 类:
- 开放寻址法: 例如 ThreadLocalMap;
- 分离链表法: 例如 HashMap。
开放寻址(Open Addressing)的核心思想是: 在出现散列冲突时,在数组上重新探测出一个空闲位置。 经典的探测方法有线性探测、平方探测和双散列探测。线性探测是最基本的探测方法,我们今天要分析的 ThreadLocal 中的 ThreadLocalMap 散列表就是采用线性探测的开放寻址法。
2. ThreadLocal类
1. 简介
ThreadLocal是Java中的一个类,用于在多线程环境下保存线程相关的变量。它提供了一种线程级别的数据隔离机制,使得每个线程都可以独立地操作自己的变量副本,而不会与其他线程产生冲突。
具体而言,使用 ThreadLocal 时,每个线程可以通过 ThreadLocal#get()
或 ThreadLocal#set()
方法访问资源在当前线程的副本,而不会与其他线程产生资源竞争。这意味着 ThreadLocal 并不考虑如何解决资源竞争,而是为每个线程分配独立的资源副本,从根本上避免发生资源冲突,是一种无锁的线程安全方法。
通常情况下,当多个线程共享同一个变量时,需要考虑线程安全性和同步问题。而使用ThreadLocal可以避免这种复杂性,因为每个线程都有自己的变量副本,互相之间不会相互干扰。
2. 实现原理浅析
线程Thread类中有一个ThreadLocalMap成员变量,一个线程所有通过ThreadLocal创建的独立变量实际上就是存放在这里面;ThreadLocalMap本身是ThreadLocal的一个内部类,可以把它理解为ThreadLocal 类实现的定制化的 HashMap。内部也是通过一个Entry数组存储键值对,键中存储对ThreadLocal对象的弱引用,一个线程第一次访问某个 ThreadLocal 变量时,就会创建ThreadLocalMap。
从图中可以看出,ThreadLocal本身并不存储value值,只是作为key在ThreadLocalMap中索引value值
3. 常用API解析
1. T set()
设置当前线程中 ThreadLocal 的变量值, 源码如下:
public void set(T value) {
//1. 获取当前线程实例对象
Thread t = Thread.currentThread();
//2. 通过当前线程实例获取到ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
map.set(this, value);
else
//4.map为null,则新建ThreadLocalMap并存入value
createMap(t, value);
}
2. void get(T value)
用于获取当前线程中 ThreadLocal 的变量值, 源码如下:
public T get() {
//1. 获取当前线程的实例对象
Thread t = Thread.currentThread();
//2. 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. 获取map中当前ThreadLocal实例为key的值的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4. 当前entitiy不为null的话,就返回相应的值value
T result = (T)e.value;
return result;
}
}
//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
return setInitialValue();
}
setInitialValue()方法又做了什么呢?
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// intialValue源码
protected T initialValue() {
return null;
}
该方法的逻辑和 set 方法几乎一样,主要来看下 initialValue 方法:
3. protected T initialValue()
该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()时才执行,并且仅执行1次(即:最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(T)方法,则不会在线程中再调用initialValue方法)。ThreadLocal中的缺省实现直接返回一个null:
使用示例:
private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0; // 初始值设置为0
}
};
该方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值
4. void remove()
作用是从当前线程的 ThreadLocalMap 中删除与当前 ThreadLocal 实例关联的值。这个方法在释放线程局部变量的资源或重置线程局部变量的值时特别有用。源码如下:
public void remove() {
//1. 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 从map中删除以当前ThreadLocal实例为key的键值对
m.remove(this);
}
使用实例:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");
Thread thread = new Thread(() -> {
System.out.println(threadLocal.get()); // 输出 "Initial Value"
threadLocal.set("Updated Value");
System.out.println(threadLocal.get()); // 输出 "Updated Value"
threadLocal.remove();
System.out.println(threadLocal.get()); // 输出 "Initial Value"
});
thread.start();
4. ThreadLocalMap 浅析
ThreadLocalMap 是 ThreadLocal 类的静态内部类,它是一个定制的哈希表,专门用于保存每个线程中的线程局部变量。
static class ThreadLocalMap {}
和大多数容器一样,ThreadLocalMap 内部维护了一个 Entry 类型的数组 类型的数组 table,长度为 2 的幂次方。
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
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<?>>
,它的 value 字段用于存储与特定 ThreadLocal 对象关联的值。使用弱引用作为键允许垃圾收集器在不再需要的情况下回收 ThreadLocal 实例。
Thread、ThreadLocal、ThreadLocalMap、Entry 之间的关系
这里我们可以用一张图来理解下:
上图中的实线表示强引用,虚线表示弱引用。每个线程都可以通过 ThreadLocals 获取到 ThreadLocalMap,而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组。
当我们为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例对象为 key,值为 Entry 往这个 ThreadLocalMap 中存放。
注意,Entry 的 key 为弱引用,意味着当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null
)时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。
这样一来,ThreadLocalMap 就会出现 key 为 null 的 Entry,也就没办法访问这些 key 对应的 value,如果线程迟迟不结束的话,这些 key 为 null 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,无法回收就会造成内存泄漏。
当然,如果 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收时都会被系统回收。但实际开发中,线程为了复用是不会主动结束的,比如说数据库连接池,过大的线程池可能会增加内存泄漏的风险,因此合理配置线程池的大小和线程的存活时间有助于减轻这个问题。
ThreadLocalMap的设计者很显然也想到了这个问题,所以其在每一次对ThreadLocalMap的set,get,remove等操作中,都会清除Map中key为null的Entry。因此,ThreadLocal一般是不会存在内存泄露风险的。
但是,将清除NULL对象的工作交给别人,并不是一个明智的选择,为了避免这个问题,在每次使用完 ThreadLocal 之后,最好明确调用 ThreadLocal 的 remove 方法来删除与当前线程关联的值。这样可以确保线程再次使用时不会存储旧的、不再需要的值。
文章引用
ThreadLocal 超强图解,这次终于懂了~ - 知乎 (zhihu.com)
https://blog.csdn.net/zhiyikeji/article/details/125473692