ThreadLocal源码分析
一、ThreadLocal概述
ThreadLocal是一个线程的本地变量,意味着这个变量是线程独有的,是不能与其他线程资源共享的。这样就可以避免其他线程资源竞争造成的线程不安全问题。
1.1 这种加锁方式与(Sync,Lock)的区别
- 关于资源问题,多线程访问共享资源或变量时,可以通过Sync/Lock 来保证资源数据的完整性,而ThreadLocal是将每个资源线程各一份来实现共享资源的数据完整性问题的
- Sycn/Lock是牺牲时间换空间,ThreadLocal是牺牲空间换时间的方式实现
二、 ThreadLocal使用说明
ThreadLocal tl = new ThreadLocal();
tl.set("testThreadLocal");
System.out.Print(tl.get());
// console ==> testThreadLocal
可以通过给ThreadLocal中设置Map从而实现ThreadLocal值的灵活性
三、ThreadLocal源码分析
变量说明
// nextHashCode => 给出下一个hashCode值
private static AtomicInteger nextHashCode =new AtomicInteger();
// 黄金比例值,所有数据与此值取模的到的值的下标散列度更均匀
// ThreadLocal中选用的乘数值为 2`32 * 黄金比例值
private static final int HASH_INCREMENT = 0x61c88647;
// 下一个HashCode值
private static AtomicInteger nextHashCode = new AtomicInteger();
核心方法说明
set
set(T t)
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 从当前线程中拿到ThreadMap
// * 说明:
// ThreadLocalMap 在ThreadLocal中定义,在Thread中生效,保证每个线程都有一份ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果ThreadLocalMap != null 说明有set过值
map.set(this, value);
else
// 如果 == null 则创建ThreadLocalMap并且给Thread实例中的ThreadLocals变量赋值
// 调用ThreadLocal.ThreadLocalMap的构造方法赋初始值和扩容因子
createMap(t, value);
}
createMap(Thread t,T firstValue)
void createMap(Thread t, T firstValue) {
// 调用ThreadLocal.ThreadLocalMap的构造方法赋初始值和扩容因子
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
new ThreadLocalMap(ThreadLocal<?> fitstKey,Object firstValue)
// firstKey: 当前创建ThreadLocal实例对象, firstValue : 首个数据
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//INITIAL_CAPACITY: 初始容量 16
table = new Entry[INITIAL_CAPACITY];
// 利用斐波那契黄金比例值进行取模得到元素下标(使用斐波那契黄金比例值的目的可以使取模后的元素数据散列度更均匀)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建Entry对象,Entry对象真正包装并存放数据的实体
table[i] = new Entry(firstKey, firstValue);
// 当前元素个数1
size = 1;
// 负载因子(扩容因子) : capacity * 2 /3
setThreshold(INITIAL_CAPACITY);
}
new Entity(ThreadLocal<?> k ,Object v)
// 注意 Entry对象 是实现了WeakReference的
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
// 确切要存储的val值
Object value;
// 将key 存放在弱引用中
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap.set(ThreadLocal<?> key, Object value);
// key: 当前ThreadLocal对象 val: 存储值
private void set(ThreadLocal<?> key, Object value) {
// 拿到所有的entity对象值
Entry[] tab = table;
// 拿到元素个数
int len = tab.length;
// 取模 黄金比例数取余容量的长度 得到下标
int i = key.threadLocalHashCode & (len-1);
// 拿到tab下标的值
for (Entry e = tab[i];
// 如果不为null,说明此下标存在数据,则给当前下标+1 在判断赋值
e != null;
// 获取下一个下标位置
e = tab[i = nextIndex(i, len)]) {
// 获取当前下标的值
ThreadLocal<?> k = e.get();
// 判断当前下标的key和传入key是否是同一个ThreadLocal实例
if (k == key) {
// key相同则覆盖val
e.value = value;
// 结束
return;
}
// 如果k==null, 说明没有该key被GC清理过了
if (k == null) {
// 清理并替换新的条目
// 如果key == null 那么大概率发生过GC了, 则向前向后遍历索引进行条目的清理避免内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// 给可赋值的i赋Entity值
tab[i] = new Entry(key, value);
// 元素数量+1
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal.ThreadLocalMap.replaceStaleEntry()
替换陈旧的数据: 该方法内存有向前遍历和向后遍历,因为ThreadLocal认为 getKey的时候 一旦key为null,大概率是发生了GC,ThreadLocal实例对象被GC掉了,但是Val值可能还存在这就造成了内存泄漏,并且该key附近的key大概率也会被GC掉,所以需要向前后遍历清理条目防止内存泄漏
// key 当前ThreadLocal实例对象 value 要存储的值 staleSlot 元素下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
get
ThreadLocal.get()
// ThreadLocal get方法
public T get() {
// 因为变量副本是每个线程独一份,所以要获取当前线程,并给当前线程实例setThreadLocals值
Thread t = Thread.currentThread();
// 获取线程实例中的 threadLocals 如果实现是InheritableThreadLocal的话那么拿到的是inheritableThreadLocals
ThreadLocalMap map = getMap(t);
// 如果map为null 说明没有set过值 赋初始值
if (map != null) {
// 获得Entry对象,如果为null则继续走初始化逻辑
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map == null 或 entry节点 == null 则set初始值
return setInitialValue();
}
ThreadLocal.getMap(Thread t)
/*
源码注释: 获取和ThreadLocal相关的ThreadLocalMap,在InheritableThreadLocal(可继承的ThreadLocal) 做了功能强化实现[实现了变量在父子线程之间的共享]
扩展知识:
问题描述: 业务场景,如果想让目标值在代码上下文中随放随取,那么ThreadLocal和InheritableThreadLocal(可继承的ThreadLocal)则满足大部分的业务场景,但是如果涉及到了线程池,因为线程池中的线程存在线程复用,那么线程中存储的值在上下文中会导致混乱,执行线程池中的任务时可能拿到其他任务的线程的val,为了适配此种场景,可以使用阿里提供的TransmittableThreadLocal,简称TTL
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
相关类: TransmittableThreadLocal
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
setInitialValue()
private T setInitialValue() {
// 在ThreadLocal中此返回为null 源码注释说明: 此方法可根据程序员在内部类中进行扩展
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 再次getMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果不为null 则设置初始值 及 key = 当前线程实例 , v = null
map.set(this, value);
else
// 如果map == null 说明该线程变量的threadLocals还处于null状态,给创建初始值
// 内部做的事情就是 Thread.currentThread().threadLocals = new ThreadLocal.ThreadLocalMap(Thread.currentThread(),null);
// 在可继承的ThreadLocals中重写成了 ->
// Thread.currentThread().inheritableThreadLocals = new ThreadLocal.ThreadLocalMap(Thread.currentThread(),null);
createMap(t, value);
// 返回 null 或 程序扩展后的initialValue()
return value;
}
getEntry(ThreadLocal<?> key)
private Entry getEntry(ThreadLocal<?> key) {
// 拿到ThreadLocalHashCode 对 table.length 取模 得到数据下标
// 因为一个线程中可能会存在多个ThreadLocal,所以此处使用的是对当前ThreadLocal实例取HashCode再对table.length取模得到下标
// 当找到此下标的时候再冲Entity中拿到Key(也就是ThreadLocal实例)比较,相同则覆盖,否则找下一个下标(开放寻址)
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);
}
remove()
remove
public void remove() {
// 从当前线程中获取ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除
m.remove(this);
}
ThreadLocal.ThreadLocalMap.remove()
private void remove(ThreadLocal<?> key) {
// 拿到所有Entry对象
Entry[] tab = table;
int len = tab.length;
// 获取当前ThreadLocal的下标
int i = key.threadLocalHashCode & (len-1);
// 遍历并对比ThreadLocal实例进行删除
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清除key
e.clear();
// 清理key == null的条目
// get() set() remove() 中都使用的此方法,对内存溢出做了有效防范
// 再重申一遍:
// 1. 此方法会通过下标遍历该节点,并tab[i]==null
// 2. 并且遍历tab[i]附近的节点key是否为null (因为ThreadLocal认为,既然走到此方法了,那么附近的数据大概率也存在有内存溢出现象)
// 2.1 先前向后遍历 直到遍历到节点key不为null为止
expungeStaleEntry(i);
return;
}
}
}
扩展
ThreadLocal的静态类
class -> ThreadLocal.SppliedThreadLocal
// 关注点 1 该内部类实现了ThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// 关注点2 该类重写了ThreadLocal的initialValue() 方法
// 该内部类的意图显而易见, ThreadLocal在没有setVal的情况下多线程使用get()方法可以拿到用户自定的默认值
@Override
protected T initialValue() {
return supplier.get();
}
}
ThreadLocal未重写的initialValue()
protected T initialValue() {
// 简单易懂 就是null
return null;
}
使用demo
public static void main(String[] args) {
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "default Data");
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"拿到threadLocal val: "+ threadLocal.get());
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"拿到threadLocal val: "+ threadLocal.get());
},"t2").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"拿到threadLocal val: "+ threadLocal.get());
},"t3").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"拿到threadLocal val: "+ threadLocal.get());
},"t4").start();
}
效果图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3FvJ7PR-1664106129230)(C:\Users\yzc\AppData\Roaming\Typora\typora-user-images\image-20220925194128840.png)]
四、ThreadLocal问题点
4.1 ThreadLocal导致内存泄漏的原因?
ThreadLocal它的组成,ThreadLocal类中存在一个内部类叫ThreadLocalMap,是软引用的实现,内部有k,v两个元素, k 既是ThreadLocal的实例对象,并属于软引用的一部分,v 则是强引用的值,存放在堆中 而ThreadLocalMap在Thread类中做声明,也就是说1. ThreadLocal的生命周期和Thread一样长, 2. 每个ThreadLocal对象在不同线程中独一份, 当发生GC时,由于ThreadLocalMap中Entry对象的key是ThreadLocal实例,属于弱引用,会被垃圾进行回收,但是Val是确确实实存在内存中,并且被ThreadLocalMap进行了引用,但是key[ThreadLocal]对象被清理掉了,所以这个对象永远也访问不到,于是就造成了内存泄漏!
4.2 ThreadLocal.ThreadLocalMap.Entity集成了WeakReference,为什么还会导致内存泄漏?
Entity对象虽然是软引用,但是真正存放软引用的值只是map中的key,也就是ThreadLocal实例对象,ThreadLocal可以被垃圾回收掉,map中变为null,但是val却永远也访问不到了
4.3 ThreadLocalMap是如何处理Hash冲突的?
使用开放寻址法,简单来说如果set之前先get,如果不为null的话获取下一个nextIndex()的下标,如果下标>= table.length 则 nextIndex = 0
4.4 对于线程池中线程复用的情况,如何保证线程复用情况下的val上下文的传递?
使用阿里提供的 TransmittableThreadLocal 简称(TTL)
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
4.5 ThreadLocal真的会造成内存泄露吗?
很难造成内存溢出, 从ThreadLocal数据结构来看,他的生命周期和线程的生命周期是息息相关的,ThreadLocal不使用set,get,remove方式清除内存的话,确实会造成内存的泄漏,但是真实业务中ThreadLocal中能够存储对象的Entity -> val 存放对象夸张来说最高也在10m-20m左右,(对象太大的话笨蛋才会把对象放在ThreadLocal中进行上下文的传递), 就算真的发生了内存泄漏,对系统的影响也是小之又小, 退一万步来说,真的对系统造成了影响,个人觉得你的关注点应该是如何在线程资源的泄漏做有效的处理