ThreadLocal

ThreadLocal

一、ThreadLocal 概述

1、概述

ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的工具。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。ThreadLocal 可以用于解决多线程中共享对象的线程安全问题。

通常我们会使用 synchronzed 或 lock 来控制线程对临界区的访问,区别在于:

  • synchronized 和 lock 是通过加锁,牺牲时间来解决访问冲突。
  • ThreadLocal 是通过每个线程都拥有自己的“共享资源”,牺牲空间来解决冲突。

所以 ThreadLocal 就是通过空间换时间,虽然内存占用变大了,但是减少了阻塞,提高了效率。

2、ThreadLocalMap

ThreadLocal 有一个静态内部类 ThreadLocalMap

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        // 底层存数据的数组
        private Entry[] table;	

        // 初始容量(必须是2的幂,默认为16)
        private static final int INITIAL_CAPACITY = 16;
        
        // table中Entry的数量
        private int size = 0;

        // 扩容阈值,当size达到阈值时会触发扩容(loadFactor=2/3;newCapacity=2*oldCapacity)
        private int threshold; // Default to 0

        // 参数:ThreadLocal对象 和 对应的value
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化table数组
            table = new Entry[INITIAL_CAPACITY];

            // 根据 ThreadLocal对象 确定 数组下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

            // 将 ThreadLocal对象 和 对应的value 存到数组指定的下标处
            table[i] = new Entry(firstKey, firstValue);

            // 初始化为1
            size = 1;

            // 设置扩容阈值(数组长度的2/3)
            setThreshold(INITIAL_CAPACITY);
        }

        // 设置扩容阈值(数组长度的2/3)
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
    }
}

3、ThreadLocalMap - Entry

ThreadLocalMap 本质是一个 Entry 类型的数组 table,而Entry继承于WeakReference弱引用。

public class ThreadLocal<T> {
    static class ThreadLocalMap {

        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);	// ThreadLocal对象是弱引用
                value = v;	// ThreadLocal的值是强引用
            }
        }
    }
}

实例化Entry时,将 ThreadLocal对象 传给父类 WeakReference的构造器

public class WeakReference<T> extends Reference<T> {
    // ThreadLocal建立弱引用
    public WeakReference(T referent) {
        super(referent);
    }
}

WeakReference 构造器又将 ThreadLocal对象 传给它的父类Reference的构造器,赋给 referent 属性

public abstract class Reference<T> {
    
    private T referent;

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;	// ThreadLocal
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

4、工作原理

每个线程(Thread)都持有一个 ThreadLocalMap 类型的实例 threadLocals

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

工作原理:

  • ThreadLocalMap 为每个 Thread 都维护了一个Entry数组(保存 ThreadLocal对象 和 对应的value
  • 根据 ThreadLocal 确定 Entry 数组的下标,从而可以找到对应的value
  • ThreadLocal 的操作,其实都是在操作 ThreadLocalMap 中的 Entry数组。

在这里插入图片描述

5、关联关系图

在这里插入图片描述

其中连线5是弱引用,其他都是强引用。

6、使用示例

public class ThreadLocalDemo {

    // 创建一个 ThreadLocal 实例
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        // 在主线程中设置线程本地变量的值
        threadLocal.set("Main Thread Value");

        // 创建并启动两个子线程
        new Thread(() -> {
            // 在子线程中获取线程本地变量的值
            String value = threadLocal.get();
            System.out.println("Thread 1 Value: " + value);
        }).start();

        new Thread(() -> {
            // 在另一个子线程中设置线程本地变量的值
            threadLocal.set("Thread 2 Value");
            // 在子线程中获取线程本地变量的值
            String value = threadLocal.get();
            System.out.println("Thread 2 Value: " + value);
        }).start();

        TimeUnit.SECONDS.sleep(1);

        // 在主线程中获取线程本地变量的值
        String mainThreadValue = threadLocal.get();
        System.out.println("Main Thread Value: " + mainThreadValue);
    }
}

代码结果如下所示:

Thread 1 Value: null
Thread 2 Value: Thread 2 Value
Main Thread Value: Main Thread Value

7、应用场景

ThreadLocal 的使用场景非常多,比如说:

  • 用于保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息。
  • 用于保存数据库连接、Session 对象等,这样在同一个线程中的任何地方都可以获取到数据库连接、Session 对象等。
  • 用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文。
  • 用于保存线程中的变量,这样在同一个线程中的任何地方都可以获取到线程中的变量。

例如:通过 Spring MVC 拦截器 将 用户信息 保存到 ThreadLocal 中

public class UserHelper {

    private static TransmittableThreadLocal<UserInfo> USER_INFO = new TransmittableThreadLocal<>();

    public static UserInfo getUserInfo() {
        return USER_INFO.get();
    }

    public static void setUserInfo(UserInfo userinfo) {
        USER_INFO.set(userinfo);
    }
    
    public static void clear() {
        USER_INFO.remove();
    }
    
}
@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        try {
            String staffId = request.getHeader("STAFF_ID");
            String tenantId = request.getHeader("TENANT_ID");
            UserInfo userInfo = new UserInfo(staffId, tenantId);
            UserHelper.setUserInfo(userInfo);
        } catch (Exception e) {
            log.error("get userInfo error", e);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) throws Exception {
        UserHelper.clear();
    }

}

二、线性探测算法

1、哈希冲突

哈希表是基于数组的,每个数组元素被称为一个“桶”(Bucket),桶中存储了键值对(Key-Value Pair),键是通过哈希函数生成的,理想的哈希函数可以均匀分布键,从而最大限度地减少冲突。

理想的哈希函数可以均匀分布键,从而最大限度地减少冲突。当两个或多个键的哈希值相同(即映射到同一个桶)时,称之为哈希冲突。常见的解决策略有拉链法(HashMap)和开放地址法(ThreadLocalMap)。

HashMap不同,ThreadLocalMap 是使用开放地址法来处理哈希冲突的,主要是因为:

  • ThreadLocalMap 中的哈希值分散的比较均匀,很少会出现冲突。
  • ThreadLocalMap 经常需要清除无用的对象,冲突的概率就更小了。

2、开放地址法

开放地址法中,若数据不能直接存放在哈希函数计算出来的数组下标时,就需要寻找其他位置来存放。在开放地址法中有三种方式来寻找其他的位置,分别是「线性探测」、「二次探测」、「再哈希法」。

  • 线性探测:当哈希函数计算出来的数组下标已经被占用时,就顺序往后查找,直到找到一个空闲的位置。

  • 二次探测:当哈希函数计算出来的数组下标已经被占用时,就按照某种规律往后查找,直到找到一个空闲的位置。

    比如说每次查找的步长是 1,2,4,8,16……

  • 再哈希法:当哈希函数计算出来的数组下标已经被占用时,就使用另一个哈希函数计算出来的数组下标。

3、开放地址法 - 线性探测

1)插入

有一个长度为8的数组,选择的hash函数是 e.key%8,这个8是指数组的长度(容量),当数组长度发生变化,hash函数也应该变化。

在这里插入图片描述

插入一个元素e1,key为5,hash函数计算出应该存到位置5(5%8=5)。发现位置5没有被占用,则将e1存入位置5:

在这里插入图片描述

又插入一个新元素e2,key为13,hash函数计算出也应该存到位置5(13%8=5),但是发现位置5已经被占用了,此时就往后看位置6有没有被占用,此时发现位置6没有被占用,则e2就存到位置6了

在这里插入图片描述

此时又插入一个新元素e3,key为21,发现还要存到位置5,但是位置5已经被占了,往后看位置6,发现位置6也被占了,再看位置7,位置7空着,所以e3就存到位置7了

在这里插入图片描述

此时又插入一个新元素e4,key为29,发现还是存到位置5,并且位置5、6、7已经都被占用了,此时只能从头考虑位置0了,发现位置0未被占用,则将e4存到位置0

在这里插入图片描述

2)查找

现在要查询key为5的元素,通过计算,对应的位置为5,查看位置5的key,发现位置5的key与要查找的key相等,则查找成功,返回e1;

如果要查询一个key为13的元素,通过计算key为13,对应位置5,但是位置5的key为5,与13不匹配,此时往后看位置6,发现位置6的key为13,与要查找的key相等,此时查找成功,返回e2即可;

如果要查询key为37的元素,通过计算对应位置5,但是位置5的key为5,与13不匹配,往后看位置6的key为13也和37不匹配,一直到位置0,发现e4的key为29,仍旧不匹配要找的37,接着看位置1,发现位置1没有元素,证明数组中没有存key为37的元素,查找失败。

在这里插入图片描述

3)删除

如果要删除一个key为13的元素,通过计算key为13,对应位置5,但是位置5的key为5,与13不匹配,此时往后看位置6,发现位置6的key为13,与要删除的key相等,于是将位置6的元素删除。

在这里插入图片描述

如果此时不进行其他修改操作,而是进行查找操作,比如查找key为21的元素,应该对应位置5,但是位置5已经有元素了,且不是要找的元素,此时会往后看下一个位置,发现位置6为空,没有元素,所以此次查询失败!!!

但是,这次查询是失败的!不是说查询的方式有问题,而是说数组的元素存放有问题,因为key为21的元素在数组中是有的,但是却并没有被查询出来。

为了解决这个问题,我们在删除元素后,要将其后面的元素进行重新确定位置,也就是rehash,过程如下:

删除的是位置6的元素,所以看位置6后面的元素,7->0->5,每个元素都需要计算hash,确定新位置。

比如位置7的key为21,发现应该调整到位置5,发现位置5已经有了元素,看位置6,发现位置6空着,则将元素e3放入位置6

在这里插入图片描述

接着看位置0的元素是否需要调整,在进行计算并且经过上面的流程后,e4应该调整到位置7

在这里插入图片描述

需要注意的是,调整位置0后,由于位置1没有元素,则可以停止调整。

  • 因为没有元素,则表示后面的第一个非空位置存的元素(比如e1)肯定没有冲突。

4)扩容

当数组中所有位置都填满了,此时再插入元素,就无处安放了,此时有两种做法:1.拒绝插入;2.扩容。

一般来说,并不是当数组没有空位时才扩容,而是数组元素达到一定阈值后就进行扩容,但是需要注意的是数组扩容要做的不只是数组扩容,还需要将旧数组中的元素拷贝到新数组中。

当数组扩容后(假设是翻倍),则数组长度变为16,下标从0~15,如下图所示:

在这里插入图片描述

在拷贝的过程中,从左往右遍历旧数组当中的元素,重新计算每个元素的hash值(扩容后,新的散列函数为 e.key%16),也就是确定每个元素在新数组中的位置,依次插入到新数组中,有冲突就按照原来的方式解决冲突即可。

  • 首先看位置0,有元素e4,key为29,则新位置为13(29%16=13),发现新数组的位置13空着,于是e4元素就放入位置13;
  • 然后从左往右遍历到元素e1,key为5,所以新位置为5(5%16=5),刚好位置5也空着,所以e1放入位置5;
  • 然后轮到元素e2,key为13,所以新位置为13(13%16=13),但是位置13上已经有了元素e4,往后看位置14,发现位置14空着,于是e2就放入位置14
  • 最后是元素e3,key为21,所以新位置为5(21%16=5),但是位置5上已经有了元素e1,往后看位置6,发现位置6空着,于是e3就放入位置6

所有元素都完成拷贝后,数组的扩容才真的完成,如下图所示:

在这里插入图片描述

三、源码分析 - ThreadLocal

ThreadLocal 的操作,其实都是在操作 ThreadLocalMap 中的 Entry数组。

1、构造方法

ThreadLocal 只有一个无参构造器。

public class ThreadLocal<T> {
    public ThreadLocal() {}
}

2、initialValue 方法

如果需要指定默认值,可以重写initialValue方法。

public class ThreadLocal<T> {

    protected T initialValue() {
        return null;
    }

}

但是要注意:initialValue() 方法并不是在 ThreadLocal 对象创建的时候执行的,而是延迟执行(具体看一下get方法)

  • 当调用 ThreadLocalget方法没有获取到值时,才会触发重写的 initialValue 方法,设置初始值并返回。

3、set 方法

ThreadLocalset 方法,本质是调用 ThreadLocalMapset 方法

public class ThreadLocal<T> {
    public void set(T value) {
        // 获取 当前线程 的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);

        if (map != null)
            // 存在 -> 调用 ThreadLocalMap 的 set方法
            map.set(this, value);
        else
            // 不存在 -> 初始化 ThreadLocalMap,并赋给 Thread 中的 threadLocals
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 初始化ThreadLocalMap,并赋给 Thread 中的 threadLocals
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
}

4、get 方法

public class ThreadLocal<T> {
    public T get() {
        // 获取 当前线程 的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);

        if (map != null) {
            // 存在 -> 调用 ThreadLocalMap 的 getEntry方法
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 返回 Entry 的 value(即ThreadLocal的值)
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }

        // 设置初始值 并 返回初始值
        return setInitialValue();
    }

    private T setInitialValue() {
        // 获取初始值
        T value = initialValue();
        
        // 下面和 ThreadLocal 的 set方法一致

        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);

        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);

        // 返回初始值
        return value;
    }   

    // 通过重写该方法指定默认值
    protected T initialValue() {
        return null;
    }
}

可以看到,initialValue() 方法并不是在 ThreadLocal 对象创建的时候执行的,而是延迟执行:

  • 当调用 ThreadLocalget方法没有获取到值时,才会触发重写的 initialValue 方法,设置初始值并返回。

5、remove 方法

public void remove() {
    // 获取 当前线程 的 ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 存在 -> 调用 ThreadLocalMap 的 remove方法
        m.remove(this);
}

一般是在ThreadLocal对象使用完后,调用ThreadLocalremove方法,在一定程度上,可以避免内存泄露;

四、源码分析 - ThreadLocalMap

1、set 方法

set() 方法就是根据 ThreadLocal 计算出数组下标 i,然后将值存储到 table[i] 中。

// java.lang.ThreadLocal.ThreadLocalMap

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;

    // 计算新元素应该放到哪个位置(这个位置不一定是最终存放的位置,因为可能会出现hash冲突)
    int i = key.threadLocalHashCode & (len - 1);

    // 判断计算出来的位置是否存在Entry,如果存在:
    // 1. 如果key就是当前的ThreadLocal,则直接更新value
    // 2. 如果key为null,说明是陈旧的Entry,则直接构建新的Entry替换之
    // 3. 如果key是其他的ThreadLocal,则使用「开放地址法-线性探测」往后找
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取Entry的key(即 ThreadLocal对象)
        ThreadLocal<?> k = e.get();

        // 判断key是否相等(判断弱引用的是否为同一个ThreadLocal对象)如果是,则更新value
        if (k == key) {
            e.value = value;
            return;
        }

        // k为null,说明ThreadLocal对象已经被GC了,当前的Entry是陈旧的(stale entry)
        if (k == null) {
            // 用新Entry替换旧Entry,同时也会清理其他陈旧的Entry,防止内存泄露
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 走到这,说明数组中没有可以复用的Entry(key就是当前ThreadLocal对象 或者 key为null)

    // 创建一个新的Entry,放入数组中
    tab[i] = new Entry(key, value);

    // Entry数组的元素数量加1
    int sz = ++size;

    // 先清理一些Slot,防止内存泄漏。
    // 1. 如果清理到了,则不需要扩容
    // 2. 如果没有清理,由于新插入了一个元素,需要判断是否需要扩容(达到阈值)
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        // 先遍历整个Entry数组清除陈旧Entry,如果还是不行,就扩容(扩容为2倍)
        rehash();
    }
}

1)replaceStaleEntry

// java.lang.ThreadLocal.ThreadLocalMap

// 用新Entry替换旧Entry,同时也会清理其他陈旧的Entry,防止内存泄露
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

2)cleanSomeSlots

// java.lang.ThreadLocal.ThreadLocalMap

/**
 * 清理一些Slot
 *
 * @param i:ThreadLocal的下标
 * @param n:Entry数组的元素个数
 * @return:是否清除了陈旧的entity
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        // entity不为null 但是 key为null -> 陈旧的entity
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);	// 删除陈旧的entity
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

3)rehash

// java.lang.ThreadLocal.ThreadLocalMap

private void rehash() {
    // 遍历整个Entry数组,删除陈旧的Entry
    expungeStaleEntries();

    // 清除完陈旧Entry后,再次判断是否需要扩容
    if (size >= threshold - threshold / 4) {
        // 扩容为原来的2倍,遍历将所有元素拷贝到扩容后的新数组中
        resize();
    }
}

【相关方法】

/**
 * 遍历整个Entry数组,删除陈旧的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);
    }
}
/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;	// 新数组为原数组的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();
            // 遍历过程中,如果遇到陈旧的Entry,直接把value置为null,便于GC
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 重新计算Entry在新数组的位置
                int h = k.ThreadLocalHashCode & (newLen - 1);
                // 如果冲突了,使用「开放地址法-线性探测」往后找
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    // 设置新哈希表的threshHold和size属性
    setThreshold(newLen);
    size = count;
    table = newTab;
}

2、getEntry 方法

get() 方法就是 根据 ThreadLocal 计算出数组下标i,然后返回 table[i] 的数据。

private Entry getEntry(ThreadLocal<?> key) {
    // 根据key的hash值(非hashCode)计算出对应Entry的位置
    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);
}
/**
 * Version of getEntry method for use when key is not found in its direct hash slot.
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // e不为空 -> key计算出的位置可能会有冲突(比如预期位置是p=5,但是p=5的位置已经有其他Entry了)
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // Entity匹配(key的hash值相同),则返回相应的Entity
        if (k == key)
            return e;
        if (k == null)
            // 删除陈旧的Entity(key为null)
            expungeStaleEntry(i);
        else
            // 使用「开放地址法-线性探测」往后找
            i = nextIndex(i, len);
        e = tab[i];
    }
    
    return null;
}

3、remove 方法

remove() 方法就是 根据 ThreadLocal 计算出数组下标i,然后移除 table[i]

/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;

    // 根据key的hash值(非hashCode)计算出对应Entry的位置
    int i = key.threadLocalHashCode & (len-1);
    
    // 判断指定下标的entry是否存在,如果不存在,使用「开放地址法-线性探测」往后找
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 将entry的key置为null(Reference的clear方法)
            e.clear();
            // 删除陈旧的Entity(key为null)
            expungeStaleEntry(i);
            return;
        }
    }
}
public abstract class Reference<T> {
    private T referent;         /* Treated specially by GC */

    public void clear() {
        this.referent = null;
    }
}

4、expungeStaleEntry 方法

expungeStaleEntry方法用于删除指定槽位的Entry

  1. 删除指定槽位的Entry:value置为null,table[i] 置为 null
  2. 从删除的槽位往后,Rehash所有Entry,放到正确的位置上,直到遇到null
/**
 * 删除指定槽位的Entry
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 删除指定槽位的Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 从删除的槽位往后,Rehash所有Entry,放到正确的位置上,直到遇到null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 删除陈旧的Entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 重新计算Entry在新数组的位置
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 换位置
                tab[i] = null;

                // 如果冲突了,使用「开放地址法-线性探测」往后找
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

五、ThreadLocal 内存泄漏问题

1、相关概念

在分析之前,先了解一下相关的概念:

1)内存溢出 & 内存泄漏

  • 内存溢出(memory overflow):要用,但是不够了,导致OOM。

  • 内存泄漏(memory lack):不用了,但是没有释放,造成系统内存的浪费。

当内存泄露到达一定规模后,造成系统能申请的内存较少,甚至无法申请内存,最终会导致内存溢出。

2)强引用 & 弱引用

  • 强引用:只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。
  • 弱引用:在系统GC时,只要发现弱引用,不管 JVM 的内存空间是否足够,都会回收只被弱引用关联的对象。

2、ThreadLocal 的 内存泄漏

先回顾一下ThreadLocal的内部关系图(其中连线5是弱引用,其他连线都是强引用)

在这里插入图片描述

假设在业务代码中已经使用完 ThreadLocal 了

  1. ThreadLocal ref 被回收了,连线4断开

  2. 由于 Entry 的 key 只持有 ThreadLocal对象 的弱引用,而此时没有任何强引用指向 ThreadLocal对象

    因此 ThreadLocal对象 就可以顺利被GC回收,连线5断开,此时Entry中的 key ref = null

  3. 但是,在 没有手动删除Entry 以及 Thread依然在运行 的前提下:

    强引用链 Thread ref -> Thread对象 -> ThreadLocalMap -> Entry -> value 始终存在

    这就导致 value不会被回收,而这块value永远也不会被访问到了,导致value发生了内存泄漏。

可以看到,虽然弱引用使得 ThreadLocal对象 可以被及时回收,但是 Entry 的 value 仍然存在内存泄漏的问题

3、内存泄漏和弱引用有关?

有些人看到 ThreadLocal 使用了弱引用,就猜想内存泄漏问题是弱引用导致的,看完上面的例子,应该知道这是不对的。

我们假设 ThreadLocal 使用强引用(也就是关系图中的连线5是强引用)

在这里插入图片描述

假设在业务代码中已经使用完 ThreadLocal 了

  1. ThreadLocal ref 被回收了

  2. 由于 Entry 的 key 对 ThreadLocal对象 是强引用,导致 ThreadLocal对象 无法被回收。

  3. 在 没有手动删除Entry 以及 Thread依然在运行 的前提下:

    强引用链 Thread ref -> Thread对象 -> ThreadLocalMap -> Entry 始终存在

    这就导致 Entry不会被回收,导致Entry发生了内存泄漏(此时Entry中包含 ThreadLocal对象 和 value)。

可见,ThreadLocal 的 内存泄漏和强引用完全没关系。

4、如何避免内存泄漏?

其实通过上面两个例子,可以发现内存泄漏的两个前提:

  1. 没有手动删除Entry
  2. Thread依然在运行

因此,避免内存泄漏也从这两点入手即可

  1. 使用完 ThreadLocal 后,调用 remove 方法删除对应的Entry,就能避免内存泄漏。

  2. 我们知道,ThreadLocalMap 是 Thread 的一个属性,

    那么如果 Thread结束了,ThreadLocalMap自然也会被GC,从根源上避免了内存泄漏。

在实际的工作中,我们肯定会使用到线程池,而线程池的线程是复用的,所以上面第二点也就比较难达成了。

因此,我们在使用完 ThreadLocal 后,一定要及时调用 remove 方法,避免内存泄漏。

5、为什么要使用弱引用?

通过之前两个案例的分析,我们知道,无论使用强引用还是弱引用,都无法避免内存泄漏的问题,那么我们为什么要使用弱引用呢?难道就是为了能及时回收 ThreadLocal对象 吗?

看过之前分析源码的两章之后,应该可以发现:

  • 在每次getsetremove时,都会清理陈旧的Entry(也就是key为null的Entry)

这就意味着,在使用完 ThreadLocal 并且 Thread依然在运行 的前提下,就算忘了调用 remove 方法,弱引用也比强引用多一层保障

  • 发生内存泄漏的value,在下一次调用 getsetremove 中的任意一个方法的时候都会被清除。

六、InheritableThreadLocal

在系统中,为了优化运行速度,会使用多线程编程,为了保证调用链ID能够在多线程间传递,需要考虑ThreadLocal的传递问题。

InheritableThreadLocal主要用于子线程创建时,自动继承父线程的ThreadLocal变量,方便信息的进一步传递。

1、Thread 类的属性

Thread类中包含 threadLocalsinheritableThreadLocals 两个属性

public class Thread implements Runnable {
    // 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 自父线程集成而来的ThreadLocalMap,主要用于父子线程间ThreadLocal变量的传递
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

其中 inheritableThreadLocals 主要存储可向子线程中传递的 ThreadLocal.ThreadLocalMap

2、Thread 的 init

在构造 Thread 时,都会调用init方法

public class Thread implements Runnable {
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
    // 每个构造都会调用,这里省略...
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    // 采用默认方式创建子线程时,inheritThreadLocals = true
    init(g, target, name, stackSize, null, true);
}
// inheritThreadLocals = true
private void init(ThreadGroup g, Runnable target, String name, long stackSize, 
                  AccessControlContext acc, boolean inheritThreadLocals) {
    // ....
    
    Thread parent = currentThread();
    
	// ....
    
    // 若此时父线程的inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    // ....
}

3、InheritableThreadLocal

InheritableThreadLocal继承了ThreadLocal,重写了以下3个方法:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    /**
     * 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 由于重写了getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 类似于getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

4、createInheritedMap

Threadinit中,调用了createInheritedMap方法,将父线程的inheritableThreadLocals传递至子线程。

public class ThreadLocal<T> {
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        // 参数是父线程的inheritableThreadLocals
        return new ThreadLocalMap(parentMap);
    }
}
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        /**
         * 该构造只被 createInheritedMap() 调用.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            // 父线程的 ThreadLocalMap 的 Entry数组
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            // 逐一复制 parentMap 的记录
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // childValue内部就是直接将传入的value返回
                        // 至于这里为什么要使用childValue,而不是直接赋值,
                        // 个人认为是为了代码的可读性
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
    }
}
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // childValue方法就是将传参直接返回
    protected T childValue(T parentValue) {
        return parentValue;
    }
}

ThreadLocalMap可知,子线程将parentMap中的所有记录逐一复制至自身线程。

5、使用示例

public class InheritableThreadLocalDemo {

    // 创建一个 InheritableThreadLocal 实例
    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        // 在主线程中设置线程本地变量的值
        threadLocal.set("Main Thread Value");

        // 创建子线程并启动
        Thread childThread = new Thread(() -> {
            // 在子线程中获取父线程中线程本地变量的值
            String value = threadLocal.get();
            System.out.println("Child Thread Value: " + value);
        });
        childThread.start();

        // 在主线程中等待子线程执行完毕
        childThread.join();

        // 在主线程中获取线程本地变量的值
        String mainThreadValue = threadLocal.get();
        System.out.println("Main Thread Value: " + mainThreadValue);
    }

}

代码结果如下所示:

Child Thread Value: Main Thread Value
Main Thread Value: Main Thread Value

可以看到,子线程获取到了父线程本地变量的值。

  • 35
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

scj1022

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值