ThreadLocal

78 阅读5分钟

ThreadLocal

  1. ThreadLocal 是什么?是做什么用的?
  2. ThreadLocal 实现原理是什么?
  3. ThreadLocalMap 是什么?
  4. 为什么ThreadLocalMap要使用弱引用?如何避免内存泄漏?

ThreadLocal 是什么?是做什么用的?

ThreadLocal 是一个 Java 类,它用来为每个线程提供一个唯一的副本,这个副本是对某个变量的引用。这个特性使得 ThreadLocal 中的变量对于每个线程来说都是独立的,即每个线程只能看到和修改自己的变量,而不能看到和修改其他线程的变量。

ThreadLocal 的主要用途是为每个线程提供一个唯一的变量副本,以便在多线程环境下实现数据的隔离和安全性。由于每个线程都有自己的变量副本,因此不会出现多个线程共享同一个变量的的情况,从而避免了线程安全问题。

ThreadLocal的使用场景包括:

  1. 在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束。
  2. 线程间数据隔离。
  3. 进行事务操作,用于存储线程事务信息。
  4. 数据库连接,Session会话管理。

ThreadLocal 实现原理是什么?

ThreadLocal 为每个线程提供一个唯一的副本。在不看源码之前,如果我们自己实现的话,很明显可以想到通过采用 Thread 类来实现,在T hread 类中定义一个 HashMap 用来存储, 在进行存储的时候, Key 和 Value 都需要自己去填写。并且而 ThreadLocal 大体上和我们实现方式差不多,只是没有采用 HashMap 进行存储,而是自己额外定义了 ThreadLocalMap 进行存储,并且 Key 也做了额外的设计

image-20231113113023624转存失败,建议直接上传图片文件

ThreadLocalMap 是什么?

ThreadLocalMap 是 ThreadLocal 类中的一个内部类,用于存储每个线程的 ThreadLocal 变量。具体来说,ThreadLocalMap 是一个 HashMap 对象,其中键为 ThreadLocal 对象,值为对应的 ThreadLocal 变量值。每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

对 Entry 进行添加的时候,需要注意一点 ThreadLocalMap 采用的是(+1)开放地址法,不是拉链法。由于是拉链法,因此对 Entry 进行删除的时候需要注意,不能简单删除 Entry,还需要对后面的元素进行进行重哈希操作

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[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {  // 由于 ThreadLocalMap 的 key 是弱引用,因此可能存在 key 被回收,但是 value 没有被回收的情况,下面这个函数就是回收所有的 value
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0); // 循环数组,如果到了最后就返回到开始
}
/*
	第一步,向前扫描,检查前面是否有 key == null 的元素,也就是 GC 留下的坑;这里用 slotToExpunge 表示第一个 key == null 的元素位置,如果没有,那么 slotToExpunge 就是当前位置的索引,也就是 staleSlot;
	第二步,向后扫描,检查是否有 key == 当前 ThreadLocal 的元素。如果有,就将该元素放入到 staleSlot 位置,同时根据 slotToExpunge 的值,更新需要清理的元素位置,从 slotToExpunge 位置开始进行清理与 rehash,然后返回;
	第三步,第二步扫描过程中最终遇到元素为 null,说明后面没有 key == 当前 ThreadLocal 的元素,停止扫描,更新 slotToExpunge 位置,然后从 slotToExpunge 位置开始,进行清理与 rehash 操作
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    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);
}

为什么ThreadLocalMap要使用弱引用?如何避免内存泄漏?

使用弱引用有助于避免内存泄漏,因为垃圾回收器可以清除不再被引用的对象。如果 ThreadLocalMap 使用强引用,那么即使对象不再被其他引用使用,只要它被存储在 ThreadLocalMap 中,垃圾回收器就无法回收它,从而导致内存泄漏。

ThreadLocalMap 有一个名为 remove 的方法,用于清除当前线程中已经不存在的 Entry。这样可以确保 ThreadLocalMap 中只存储当前线程中有效的 Entry,避免因为条目无效而导致的内存泄漏问题。