写点什么

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

用户头像
Android架构
关注
发布于: 8 小时前

深入理解 ThreadLocal

用途

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


public c


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


lass Test {


private static ThreadLocal<String> 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 的工作原理,我们总结一下


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

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

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

ThreadLocalMap

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


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


static class Entry extends WeakReference<ThreadLocal<?>> {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 后移的方式。


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


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

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


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

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

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


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

  2. 检查是否扩容。


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


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


这段代码非常简单。有个全局的计数器 nextHashCode,每有一个 ThreadLocal 产生这个计数器就会加 0x61c88647,然后把当前值赋给 threadLocalHashCode。关于 0x61c88647 这个神奇的常量,可以点这里

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
详解BAT面试必问之ThreadLocal(源码+内存)