ThreadLocal最全最详细的总结

ThreadLocal最全最详细的总结

一、ThreadLocal是什么?

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

ThreadLocal线程副本

1)创建

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

//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();

2)写入

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

localVariable.set("曼巴”);

3)读取

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

localVariable.get();

二、你在工作中用到过ThreadLocal吗?

ThreadLocal的可使用场景有很多,如下:

ThreadLoca存放用户上下文

  1. Web应用中的用户身份认证
    在 Web 应用中,可以使用 ThreadLocal 存储当前用户的身份信息,这样在整个请求处理过程中都可以方便地访问用户的身份信息,而不必传递参数。
  2. 数据库连接管理
    在多线程环境下,每个线程可能需要访问数据库,并且通常使用连接池来管理数据库连接。可以使用 ThreadLocal 存储数据库连接对象,保证每个线程都能获取到自己的连接,而不会与其他线程发生冲突。
  3. 事务管理
    在某些情况下,需要在多个方法调用之间共享事务上下文。可以使用 ThreadLocal 存储事务对象,在同一个线程内的方法调用中共享事务对象,从而实现事务的传播和管理。
  4. 线程安全的日期格式化
    SimpleDateFormat 是线程不安全的类,如果多个线程同时访问一个 SimpleDateFormat 实例,可能会导致格式化错误。可以使用 ThreadLocal 创建线程局部的 SimpleDateFormat 实例,确保每个线程都有自己的实例,从而避免线程安全问题。
  5. 跟踪日志
    在多线程环境下,需要在日志中记录每个线程的执行情况。可以使用 ThreadLocal 存储日志上下文信息,如请求ID、用户ID等,确保每个线程的日志都能正确地记录上下文信息。

三、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的本质是一个个<key,value>形式的节点组成的数组,那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结构图

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

1)Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。

2) ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。

3)每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

4)ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。

四、ThreadLocal 内存泄露是怎么回事?

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

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

ThreadLocal内存分配

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。

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

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

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

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

ThreadLocal<String> localVariable = new ThreadLocal();
try {
    localVariable.set("曼巴”);
    ……
} finally {
    localVariable.remove();
}

那为什么key还要设计成弱引用?

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

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

五、ThreadLocalMap的结构了解吗?

ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组散列方法

ThreadLocalMap结构示意图

  • 元素数组

    一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。

 private Entry[] table;
  • 散列方法

    散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。

int i = key.threadLocalHashCode & (table.length - 1);

这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

六、ThreadLocalMap怎么解决Hash冲突的?

我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。

ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

ThreadLocalMap解决冲突
如上图所示,如果我们插入一个value=5的数据,通过 hash计算后应该落入第 5 个槽位中,而槽位 5 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。

在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

七、ThreadLocalMap扩容机制了解吗?

从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?

在set方法中会调用rehash方法:

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) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
 
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。

threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:
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);
}

调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。

换句话说当sz大于等于10时,就可以考虑扩容了。

rehash代码如下:

private void rehash() {
    //先尝试回收一次key为null的值,释放出一些空间
    expungeStaleEntries();
 
    if (size >= threshold - threshold / 4)
        resize();
}

在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。

如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。

计算公式如下:

16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8

也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。

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;
}

resize中每次都是按2倍的大小扩容。

扩容的过程如下图所示:

在这里插入图片描述

扩容的关键步骤如下:

老size + 1 = 新size
如果新size大于等于老size的2/3时,需要考虑扩容。
扩容前先尝试回收一次key为null的值,腾出一些空间。
如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
每次都是按2倍的大小扩容。

八、父子线程怎么共享数据?

父线程能用ThreadLocal来给子线程传值吗?答案是:不能。那该怎么办?

这时候可以用到另外一个类——InheritableThreadLocal

使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。

public class InheritableThreadLocalTest {
    
    public static void main(String[] args) {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        // 主线程
        threadLocal.set("不擅打篮球");
        //子线程
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("曼巴 ," + threadLocal.get());
            }
        };
        t.start();
    }
}

那原理是什么呢?

原理很简单,在Thread类里还有另外一个变量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在Thread.init的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
ThreadLocal源码是用于在每个线程中存储值的一个类。在JDK8之前,ThreadLocal的实现主要分为两部分:ThreadLocal类和ThreadLocalMap类。ThreadLocal类是一个抽象类,其中包含了一些核心方法,如get()、set()、remove()等。而ThreadLocalMap类则是ThreadLocal的内部结构,用于存储每个线程中的ThreadLocal变量和对应的值。 在每个Thread类中都维护了一个threadLocals变量,它是一个ThreadLocal.ThreadLocalMap类型的变量,用于存储当前线程中所有的ThreadLocal变量和对应的值。当我们使用ThreadLocal对象时,实际上是通过Thread类的threadLocals变量来获取和设置对应的值。 具体地说,每个ThreadLocal对象都有一个唯一的hash值,并且这个hash值被用作ThreadLocalMap的下标。在ThreadLocalMap中使用数组来存储ThreadLocal对象和对应的值。这样一来,一个线程中可以设置多个ThreadLocal对象,并且每个ThreadLocal对象都可以独立地存储和获取值。 总结起来,ThreadLocal源码的主要作用是为每个线程提供一个独立的存储空间,用于存储线程私有的值。通过ThreadLocal类和ThreadLocalMap类的配合,我们可以方便地在多线程环境下实现线程安全的数据共享。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [ThreadLocal_ThreadLocal源码分析_](https://download.csdn.net/download/weixin_42666807/25851858)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [【源码篇】threadlocal源码超详细讲解](https://blog.csdn.net/qq_21046665/article/details/124211071)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [ThreadLocal源码深度详解](https://blog.csdn.net/A_art_xiang/article/details/131224830)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值