ThreadLocal

ThreadLocal 是什么?

ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

  • 创建ThreadLocal

创建了一个 ThreadLoca 变量 localVariable,任何一个线程都能并发访问localVariable。

//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
  • 设置ThreadLoacl 的值

线程可以在任何地方使用 localVariable,写入变量。

localVariable.set("zgj”);
  • 获取ThreadLoacl 的值

线程在任何地方读取的都是它写入的变量。

String value = localVariable.get();
  • 删除ThreadLoacl 的值
localVariable.remove();

Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。

数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接从而避免了多线程竞争同一数据库连接的问题。

格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。

ThreadLocal 有什么优点?

每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。

ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在方法间传递参数。

ThreadLocal 是如何实现的?

我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程再获取ThreadLocalMap,然后把元素存到这个map中。

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();  
    //获取ThreadLocalMap        
    ThreadLocalMap map = getMap(t);   
    //讲当前元素存入map    
    if (map != null)         
        map.set(this, value);     
    else        
        createMap(t, value);    
}

ThreadLocal实现的秘密都在这个ThreadLocalMap了,可以Thread类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals。

public class Thread implements Runnable {
    //ThreadLocal.ThreadLocalMap是Thread的属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap既然被称为Map,那么毫无疑问它是<key,value>型的数据结构。我们都知道map的本质是一个个形式的节点组成的数组,那ThreadLocalMap的节点是什么样的呢?

static class Entry extends WeakReference<ThreadLocal<?>> { 
    /** The value associated with this ThreadLocal. */   
    Object value;
    //节点类       
    Entry(ThreadLocal<?> k, Object v) {        
        //key赋值              
        super(k);         
        //value赋值          
        value = v;      
    }     
}

这里的节点,key可以简单低视作ThreadLocal,value为代码中放入的值,当然实际上key并不是ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了WeakReference(弱引用),再来看一下key怎么赋值的:

public WeakReference(T referent) { 
    super(referent);    
}

key的赋值,使用的是WeakReference的赋值。

所以,怎么回答ThreadLocal原理?要答出这几个点:

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值
  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。

ThreadLocal 为什么会出现内存泄漏?

我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。

所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,但 value 是强引用。

“弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。”

那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。

因为当ThreadLocal对象使用完之后,应该要把设置的kèy,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMapThreadLocalMap也是通过强引用指向Entry对象线程不被回收Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象

那怎么解决内存泄漏问题呢?

很简单,使用完 ThreadLocal后,及时调用 remove()万法释放内存空间,

ThreadLocal<String> localVariable = new ThreadLocal();
try {
    localVariable.set("zgj”);
    ……// 执行业务代码
} finally {
    localVariable.remove();
}

remove()方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 的 hash 值
    int i = key.threadLocalHashCode & (len-1);
    // 遍历数组,找到 key 为 null 的 Entry
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 将 key 为 null 的 Entry 清除
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}
那为什么key还要设计成弱引用?

key设计成弱引用同样是为了防止内存泄漏。

假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLocal的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

你了解哪些 ThreadLocal 的改进方案?

在JDK 20 Early-Access Build 28 版本中,出现了 ThreadLocal 的改进方案,即 scopedvalue

还有 Netty 中的 FastThreadLocal,它是 Netty 对 ThreadLocal 的优化,内部维护了一个索引常量index,每次创建 FastThreadLocal 中都会自动+1,用来取代 hash 冲突带来的损耗,用空间换时间。

private final int index;

public FastThreadLocal() {
    index = InternalThreadLocalMap.nextVariableIndex();
}
public static int nextVariableIndex() {
    int index = nextIndex.getAndIncrement();
    if (index < 0) {
        nextIndex.decrementAndGet();
    }
    return index;
}

以及阿里的 TransmittableThreadLocal,不仅实现了子线程可以继承父线程 ThreadLocal 的功能,并且还可以跨线程池传递值。

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// 在父线程中设置
context.set("value-set-in-parent");

// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();

ThreadLocalMap 的结构了解吗?(源码角度)

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);  // 这里的 Key 是 WeakReference
            value = v;
        }
    }

    private Entry[] table;  // 存储 ThreadLocal 变量的数组
    private int size;       // 当前 Entry 数量
    private int threshold;  // 触发扩容的阈值
}

底层的数据结构也是数组,数组中的每个元素是一个Entry 对象,Entry 对象继承了WeakReference,key是ThreadLocal 对象,value 是线程的局部变量。

当调用ThreadLocal.set(value)时,会将 value 存入 ThreadLocalMap。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

set() 方法是 ThreadLocalMap 的核心方法,通过 key 的哈希码与数组长度取模,计算出 key 在数组中的位置,这一点和 HashMap 的实现类似。(散列算法)

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1); // 计算索引

    for (Entry e = tab[i]; e != null; e = tab[nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) { // 如果 key 已存在,更新 value
            e.value = value;
            return;
        }
        if (k == null) { // Key 为 null,清理无效 Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    tab[i] = new Entry(key, value); // 直接插入 Entry
    size++;
    if (size >= threshold) {
        rehash();
    }
}

threadLocalHashCode 的计算有点东西,每创建一个 ThreadLocal 对象,它就会新增一个黄金分割数,可以让哈希码分布的非常均匀。

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

当调用 ThreadLoca1.get()时,会调用ThreadLocalMap的 getEntry()方法,根据 key 的哈希码找到对应的线程局部变量。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];

    if (e != null && e.get() == key) { // 如果 key 存在,直接返回
        return e;
    } else {
        return getEntryAfterMiss(key, i, e); // 继续查找
    }
}

当调用 ThreadLocal.remove()时,会调用ThreadLocalMap 的remove()方法,根据 key 的哈希码找到对应的线程局部变量,将其清除,防止内存泄漏。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);
    
    for (Entry e = tab[i]; e != null; e = tab[nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear(); // 清除 WeakReference
            e.value = null; // 释放 Value
            expungeStaleEntries();
            return;
        }
    }
}

ThreadLocalMap 如何扩容的?

在 ThreadLocalMap.set() 方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值 (len*2/3) ,就开始执行 rehash() 逻辑:

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

再着 看rehash() 具体实现:这里会先去清理过期的 Entry,然后还要根据条件判断 size >= threshold - threshold / 4 也就是 size >= threshold * 3/4 来决定是否需要扩容。

private void rehash() {
    //清理过期Entry    
    expungeStaleEntries();
    //扩容
    if (size >= threshold - threshold / 4)
        resize();
}

//清理过期Entry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

接着看看具体的resize()方法,扩容后的 newTab 的大小为老数组的两倍,然后遍历老的table数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的 newTab ,遍历完成之后,oldTab中所有的 entry 数据都已经放入到 newTab 中了,然后 table 引用指向 newTab

具体代码:

private void resize(){
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen =oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    for(int j=0;j< oldLen; ++j){
        Entry e = oldTab[j];
        if(e != null){
            ThreadLocal<?> k= e.get();
            if(k == null){
                e.value = null;// Help the GC
            } else {
                int h=k.threadLocalHashCode &(newLen -1);
                while(newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h]=e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值