• 详解 BAT 面试必问之 ThreadLocal(源码 + 内存)


    image.png
    深入理解 ThreadLocal
    用途
    我们一般用 ThreadLocal 来提供线程局部变量。线程局部变量会在每个 Thread 内拥有一个副本,Thread 只能访问自己的那个副本。文字解释总是晦涩的,我们来看个例子。

    public c

    lass Test
    {
    private static ThreadLocal threadLocal = new ThreadLocal<>();
    public static void main(String[] args)
    {
    Thread thread1 = new MyThread(“lucy”);
    Thread thread2 = new MyThread(“lily”);
    thread1.start();thread2.start();
    }
    private static class MyThread extends Thread
    {

    MyThread(String name)
    {
    super(name);
    }

    @Overridepublic void run()
    {
    Thread thread = Thread.currentThread();
    threadLocal.set("i am " + thread.getName());
    try
    {
    //睡眠两秒,确保线程 lucy 和线程 lily 都调用了 threadLocal 的 set 方法。Thread.sleep(2000);
    }
    catch (InterruptedException e)
    {
    e.printStackTrace();
    }
    System.out.println(thread.getName() + " say: " + threadLocal.get());
    }
    }
    }

    这个例子非常简单,就是创建了 lucy 和 lily 两个线程。在线程内部,调用 threadLocal 的 set 方法存入一字符串,睡眠 2 秒后输出线程名称和 threadLocal 中的字符串。我们运行这单代码,看一下输出内容。

    lucy say: i am lucylily say: i am lily

    原理
    上面例子很好的解释了 ThreadLocal 的作用,接下来我们分析一下这是如何实现的。

    我们定位到 ThreadLocal 的 set 方法。源码中 set 方法被拆分为几个方法,为了表述方便笔者将这几个方法进行了整合。

    public void set(T value)
    {
    //获取当前线程 Thread t = Thread.currentThread();
    //获取当前线程的 ThreadLocalMapThreadLocalMap map = t.threadLocals;
    if (map != null)
    //将数据放入 ThreadLocalMap 中,key 是当前 ThreadLocal 对象,值是我们传入的 value。map.set(this, value);
    else
    //初始化 ThreadLocalMap,并以当前 ThreadLocal 对象为 Key,value 为值存入 map 中。
    t.threadLocals = new ThreadLocalMap(this, value);
    }

    通过上面这段代码可以看到,ThreadLocal 的 set 方法主要是通过当前线程的 ThreadLocalMap 实现的。ThreadLocalMap 是一个 Map,它的 key 是 ThreadLoacl,value 是 Object。

    TreadLocal 的 get 方法的源码我就不贴出来了,大体上与 set 方法类似,就是先获取到当前线程的 ThreadLocalMap,然后以 this 为 key 可以取得 value。

    到这里我们基本上明白了 ThreadLocal 的工作原理,我们总结一下

    每个 Thread 实例内部都有一个 ThreadLocalMap,ThreadLocalMap 是一种 Map,它的 key 是 ThreadLocal,value 是 Object。

    ThreadLocal 的 set 方法其实是往当前线程的 ThreadLocalMap 中存入数据,其 key 是当前 ThreadLocal 对象,value 是 set 方法中传入的值。

    使用数据时,以当前 ThreadLocal 为 key,从当前线程的 ThreadLocalMap 中取出数据。

    ThreadLocalMap
    上面我们介绍了 ThreadLocal 主要是通过线程的 ThreadLocalMap 实现的。

    static class ThreadLocalMap
    {
    private ThreadLocal.ThreadLocalMap.Entry[] table;

    static class Entry extends WeakReference> {Object value;

    Entry(ThreadLocal var1, Object var2)
    {
    super(var1);this.value = var2;
    }
    }
    }

    ThreadLocalMap 是一种 Map,其内部维护着一个 Entry[]。

    ThreadLocalMap 其实是就是将 Key 和 Value 包装成 Entry,然后放入 Entry 数组中。我们看一下它的 set 方法。

    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)
    {
    //如果已经存在,直接替换 valuee.value = value;return;}

    if (k == null)
    {
    //如果当前位置的 key ThreadLocal 为空,替换 key 和 value。下文 ThreadLocal 内存分析中会提到为什么会有这段代码。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);
    }

    到这里,如果你了解 HashMap,应该可以看出 ThreadLocalMap 就是一种 HashMap。不过它并没有采用 java.util.HashMap 中数组+链表的方式解决 Hash 冲突,而是采用 index 后移的方式。

    我们简单分析一下这段代码:

    通过 ThreadLocal 的 threadLocalHashCode 与当前 Map 的长度计算出数组下标 i。

    从 i 开始遍历 Entry 数组,这会有三种情况:

    Entry 的 key 就是我们要 set 的 ThreadLocal,直接替换 Entry 中的 value。

    Entry 的 key 为空,直接替换 key 和 value。

    发生了 Hash 冲突,当前位置已经有了数据,查找下一个可用空间。

    找到没有数据的位置,将 key 和 value 放入。

    检查是否扩容。

    我们知道,HashMap 是一种 get、set 都非常高效的集合,它的时间复杂度只有 O(1)。但是如果存在严重的 Hash 冲突,那 HashMap 的效率就会降低很多。我们通过上段代码知道,ThreadLocalMap 是通过 key.threadLocalHashCode & (len-1)计算 Entry 存放 index 的。len 是当前 Entry[]的长度,这没什么好说的。那看来秘密就在 threadLocalHashCode 中了。我们来看一下 threadLocalHashCode 是如何产生的。

    public class ThreadLocal
    {
    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);}}
    这段代码非常简单。有个全局的计数器 nextHashCode,每有一个 ThreadLocal 产生这个计数器就会加 0x61c88647,然后把当前值赋给 threadLocalHashCode。关于 0x61c88647 这个神奇的常量

    【这里想说,因为自己也走了很多弯路过来的,所以才下定决心整理,收集过程虽不易,但想到能帮助到一部分自学或者是想提升java技术、成为Java架构师,提升技术P5-P6-P7-P8 的人,心里也是甜的!有需要的伙伴请点㊦方】↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

  • 相关阅读:
    Mysql数据库 5.SQL语言聚合函数 语言日期-字符串函数
    Redis SCAN命令操作实战(详细)
    【网络编程】TCP Socket编程
    目录启示:PHP 中 命名空间及其成员的访问
    Red Hat 6安装Oracle Linux内核头文件
    结构型模式
    Python sort 自定义函数排序
    将文件名称中空格以左的部分全部删除重命名
    springmvc拦截器与全局异常
    数据结构(C++)- 模拟实现跳表SkipList(Redis内部数据结构),跳表与哈希表,红黑树对比
  • 原文地址:https://blog.csdn.net/m0_60721514/article/details/127803056