ThreadLocal

文章详细介绍了ThreadLocal的工作原理,包括它的内存模型、基本操作如get、set和remove,以及关键设计点如哈希算法的选择和线性探测法解决冲突。此外,还讨论了ThreadLocal在全链路追踪、Spring事务管理等场景的应用,以及可能引发的内存泄漏问题和处理策略。
摘要由CSDN通过智能技术生成

背景

最近要做一个 ThreadLocal 的分享,把公众号,google,百度上的大多数 threadlocal 都扫了一遍,也收到公司专家的指导,于是有了这边 ThreadLocal 的文章。

历史

ThreadLocal 是解决多线程共享变量的一种方式,绝大多数主流的编程语言都支持 ThreadLocal。Java 更是在 JDK 1.2(1998 年)就支持 ThreadLocal。

引子

通过如下例子,展示 TheadLocal 是如何实现的。

public class ThreadLocalTest {
    public void testThreadLocal() throws InterruptedException {
        ThreadLocal<String> threadLocal1 = ThreadLocal.withInitial(() -> "000");
        Thread t1 = new Thread(() -> {
            threadLocal1.set("ABC");
            System.out.println("thread " + Thread.currentThread() + " " + threadLocal1.get());
        });
        threadLocal1.set("abc");
        t1.start();
        t1.join();
        System.out.println("thread " + Thread.currentThread() + " " + threadLocal1.get());
    }

    public static void main(String[] args) throws InterruptedException {
        new ThreadLocalTest1().testThreadLocal();
    }
}

如上图所示
1、创建了一个ThreadLocal对象threadLocal1,并初始化其值为"000"。
2、在主线程中,将threadLocal1的值设置为"abc"。
3、启动线程t1。
4、线程t1开始执行,将threadLocal1的值设置为"ABC"。
5、线程t1打印出自己的线程名和threadLocal1的值,即"ABC"。
6、主线程等待线程t1执行完毕。
7、主线程打印出自己的线程名和threadLocal1的值,即"abc"。

内存模型

threadlocal 内存分布
如上图所示
1、每个线程包含一个 TheadLocalMap 用以保存当前线程的 ThreadLocal 对象
2、每个线程的多个 TheadLocal 对象以数组的形式保存
3、同一 TheadLocal 在不同的对象使用不同的 ThreadLocalMap
4、不同线程共享 ThreadLocal 的 threadlocalHashCode
5、ThreadLocalMap 中的每个元素为 Entry,Entry 包括 key 和 value。其中 key 为 threadlocal,value 为threadlocal对应的值

对应代码如下:

Thread
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        private int threshold;
        
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
ThreadLocal
public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

基本操作

get()

1、获取当前线程的 threadlocals
2、获取 threadlocal 对应的 value

    public T get() {
        // 当前所在线程
        Thread t = Thread.currentThread();
        // map 对应 t.threadLocals;
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
set()

1、获取当前线程的 threadlocals
2、将 this、value 保存到 threadlocals

    public void set(T value) {
        // 当前所在线程
        Thread t = Thread.currentThread();
        // map 对应 t.threadLocals;
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
remove()

1、获取当前线程的 threadlocals
2、删除 this 对应的 entry

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

关键设计点

ThreadLocalMap vs HashMap
维度threadLocalMapHashMap
数据结构数组数组
初始容量1632
hash 算法斐波那契数hashCode
hash 冲突开放定址法-线性探测开链法+红黑树
扩容条件容量2/3,回收之后大于1/2容量负载因子
扩容大小2倍2倍
hash 算法

HASH_INCREMENT 也不是随便取的,它转化为十进制是 1640531527,2654435769 转换成 int 类型就是 -1640531527,2654435769 等于 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黄金分割数,近似为 0.618,也就是说 0x61c88647 理解为一个黄金分割数乘以 2 的 32 次方,它可以保证 nextHashCode 生成的哈希值,均匀的分布在 2 的幂次方上,且小于 2 的 32 次方。

0x61c88647 是一个“魔数”,即斐波那契数,这个特定的数值被选定的原因在于它是黄金分割比(Golden Ratio)的近似整数表示。

这个常数的二进制表示形式是:0110 0001 1100 1000 1000 0110 0100 0111,可以看到,它是一个31位的数,最高位为0,这保证了当它用作增量时,计算出的 hash 值总是正数。

那么,为什么要选择黄金分割比的近似表示呢?这是因为黄金分割比有一个特性:它可以帮助实现更好的散列分布。当我们用这个数值作为增量时,即使原始的 hash 值存在规律(例如,都是偶数),经过增量计算后的 hash 值也将尽可能均匀地分布在整个 hash 表中。

此外,选择这个特定的数也是因为它与 2^32 的除数接近黄金分割比(0.6180339887),可以为 hash 值提供良好的分布。

在 ThreadLocal 类中,每个线程都有一个唯一的线程局部变量,并且每个线程局部变量都需要一个唯一的 hash 值。通过将这个“魔数”作为 hash 值的增量,可以确保每个线程局部变量的 hash 值都是独一无二的,同时也可以尽可能地减少 hash 冲突。

如下示例

import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;

public class HashTest {
    private static final int HASH_INCREMENT = 0x61c88647;

    private static int getThreadLocalHashCode(ThreadLocal<String> threadLocal) {
        Field field = ReflectionUtils.findField(ThreadLocal.class, "threadLocalHashCode");
        field.setAccessible(true);
        return (int) ReflectionUtils.getField(field, threadLocal);
    }

    public static void dumpHashElements(int max) {
        System.out.println("hashCode    threadLocalHashCode");
        for (int i = 0; i < max; i++) {
            System.out.print((i * HASH_INCREMENT) & (max - 1));
            System.out.print("           ");
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            System.out.println(getThreadLocalHashCode(threadLocal) & (max - 1));
        }
        System.out.println(" ");
    }

    public static void main(String[] args) {
        dumpHashElements(16);
        dumpHashElements(32);
    }
}
hash 冲突解决

采用线性探测法

        private Entry getEntry(ThreadLocal<?> key) {
            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);
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
  
        // 线性探测法
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
扩容条件是什么?如何扩容?

1、没有元素需要清理,并且容量大于 2/3,探测式容量达到阈值的 3/4
2、每次扩容为当前的 2 倍

        private void rehash() {
            expungeStaleEntries();

            // 容量大于等于 3/4 时触发扩容
            if (size >= threshold - threshold / 4)
                resize();
        }

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            // 容量为之前的 2 倍
            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;
        }

更复杂的例子

ThreadLocal 的可以是各种类型。如下例子展示了 Integer,String、Map 类型

import java.util.HashMap;
import java.util.Map;

public class ThreadLocalTest {
    public static void dump(String id, ThreadLocal<Integer> threadLocal1, ThreadLocal<String> threadLocal2, ThreadLocal<Map<String, String>> threadLocal3) {
        System.out.println("thread " + id + " threadlocal1 " + threadLocal1.get());
        System.out.println("thread " + id + " threadlocal2 " + threadLocal2.get());
        System.out.println("thread " + id + " threadlocal3 " + threadLocal3.get());
    }

    public static void testThreadLocal() {
        final ThreadLocal threadLocal1 = ThreadLocal.withInitial(() -> 0);
        final ThreadLocal<String> threadLocal2 = java.lang.ThreadLocal.withInitial(() -> "000");
        final ThreadLocal<Map<String, String>> threadLocal3 = java.lang.ThreadLocal.withInitial(HashMap::new);

        Thread t1 = new Thread(() -> {
            threadLocal1.set(10);
            threadLocal2.set("abc");
            dump("t1", threadLocal1, threadLocal2, threadLocal3);
        });

        Thread t2 = new Thread(() -> {
            threadLocal1.set(20);
            threadLocal2.set("ABC");
            dump("t1", threadLocal1, threadLocal2, threadLocal3);
        });

        Thread t3 = new Thread(() -> {
            threadLocal1.set(30);
            Map<String, String> val = new HashMap<>();
            val.put("123", "XXX");
            threadLocal3.set(val);
            dump("t1", threadLocal1, threadLocal2, threadLocal3);
        });

        t1.start();
        t2.start();
        t3.start();
    }

    public static void main(String[] args) {
        testThreadLocal();
    }
}

输出

thread t2 threadlocal1 20
thread t2 threadlocal2 ABC
thread t1 threadlocal1 10
thread t3 threadlocal1 30
thread t3 threadlocal2 000
thread t1 threadlocal2 abc
thread t2 threadlocal3 {}
thread t1 threadlocal3 {}
thread t3 threadlocal3 {123=XXX}

threadlocal1

应用场景

条件:每个线程 threadlocal 数量笔记少

1、全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
2、Spring 事务管理器采用了 ThreadLocal
3、Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal

疑问

1、ThreadLocalMap 的 Entry 为什么继承自 WeakReference?
2、每个线程 threadlocal 的数量很多会有什么问题?如何处理? 速度变慢(线性探测法,数据清理)
3、如果当前线程创建子线程,当前线程的 ThreadLocal 数据可以传递给子线程的 ThreadLocal 么?为什么?
4、thread 如果退出,threadlocalMap 可以回收么?
5、thread 长时间运行(比如线程池),threadlocalMap 中的值没有引用了,如何处理,不处理会有什么问题?
6、theadlocal 中 Entry 中为什么需要 key 为 threadlocal?

内存泄漏

ThreadLocalMap 的 Entry 为什么继承自 WeakReference?

假设为强引用,threadlocal 不再使用,但是 TheadLocalMap 到 Entry,Entry 到 key 的强引用一直存在,那么,就导致内存泄漏。

public class ThreadLocalDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Thread t = new Thread(() -> test("abc", false));
        t.start();
        t.join();
        System.out.println("--gc后--");
        Thread t2 = new Thread(() -> test("def", true));
        t2.start();
        t2.join();
    }

    private static void test(String input, boolean isGC) {
        try {
            new ThreadLocal<>().set(input);
            if (isGC) {
                System.gc();
            }
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object threadLocalMap = field.get(t);
            Class<?> tlmClass = threadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(threadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);
                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们应用中,线程常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!
threadlocal weakreference

启发式清理
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter 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) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

最后

赶紧打开你们项目的代码,看看 ThreadLocal 的使用是不是有问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值