带你掌握被大厂面试官狂轰乱炸的ThreadLocal原理,别

小知识,大挑战!本文正在参与“  程序员必备小知识  ”创作活动

本文同时参与 「掘力星计划」  ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm...,日子还很长,让我们一起加油努力叭🌈


前言

博主 常年游荡于牛客面经区,总结了字节、阿里、百度、腾讯、美团等等大厂的高频考题,之后会逐步分享给大家,期待各位的关注、点赞!

在这里插入图片描述


什么是ThreadLocal?

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰


ThreadLocal怎么使用?

ThreadLocl使用比较简单,主要有三个方法:get()、set()、remove()

相信不用我多说各位小伙伴也知道是什么意思

public class Test01 {


    public static void main(String[] args) {

        ThreadLocal<Integer> local = new ThreadLocal<>();

        local.set(10);
        System.out.println(local.get());

        local.remove();
        System.out.println(local.get());

    }
}
复制代码

在这里插入图片描述


ThreadLocal底层原理

点开 ThreadLocal 的 set()方法

  • 首先是获取到当前线程
  • 调用getMap(t)方法获取到 ThreadLocalMap
  • 如果map不为null则进行set操作,如果为null则进行创建map操作
public void set(T value) {
	
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
复制代码

点开get()方法

跟HashMap差不多,小伙伴们自己看看源码绝对能懂

public T get() {
    Thread t = Thread.currentThread();
    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();
}

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);
 }
复制代码

点开remove()方法

跟HashMap差不多,找到索引,遍历又对因key就删除

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

private void remove(ThreadLocal<?> key) {
	   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)]) {
	       if (e.get() == key) {
	           e.clear();
	           expungeStaleEntry(i);
	           return;
	       }
	   }
}
复制代码

细心的小伙伴可能就发现了这三个方法中都需要去获取:==当前Thread和ThreadLocalMap==

那么这是为什么呢???

我们接着看源码,点开getMap()方法

可以发现,ThreadLocalMap获取的是Thread中的一个对象

相信到这大家就明白了 ThreadLocal是怎么做到各个线程互不干扰的把😁

因为获取到的是当前线程的ThreadLocalMap,各个线程所以互不干扰

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码

在这里插入图片描述

小伙伴们呢可别觉得已经掌握了,还没完呢,这才哪到哪,😁,我们接着看

打开ThreadLocalMap的set方法

private void set(ThreadLocal<?> key, Object value) {

     Entry[] tab = table;
     int len = tab.length;
     //hash获取对应下标,之后会讲
     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;
         }
     }
		
	//封装为Entry节点
     tab[i] = new Entry(key, value);
     int sz = ++size;
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
         rehash();
 }
复制代码

看过上面源码,熟悉HashMap的小伙伴们可能就发现了一个熟悉的身影——>==Entry==

但这个Entry就跟HashMap的有所不同,它==继承了 WeakReference<ThreadLocal<?>>==

WeakReference:弱引用

而上面还有段代码不知道小伙伴们注意到没:tab[i] = new Entry(key, value);

这段代码意味着什么相信也不用我多说了吧,就是将key,value封装为 Entry节点;再根据map.set(this, value);这段代码可知,==Key为当前的ThreadLocal对象,value为我们要set进来的值==

但是这里又有所重点:那就是对key调用了super(k),这里我暂且不多说,涉及到ThreadLocal的一个经典面试题,文章后面会进行详细讲解,小伙伴们要有耐心继续看哦!!!

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

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

到此基本结束,不要奇怪,ThreadLocal其实并不是太难,那我们先来做个总结!


总结

我们set进去的值,最终是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。


ThreadLocal相关面试题


ThreadLocal 如何解决 Hash 冲突?

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

在这里插入图片描述

使用CAS,每次增加固定的值,所以采用的是线性探测法解决HasH冲突

在这里插入图片描述

经典CAS

不懂CAS的可以看我这篇,写的超详细

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
复制代码


ThreadLocal内存泄漏问题及解决办法

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是一强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。

在这里插入图片描述

==解决办法==

1. 使用完后记得remove

2.ThreadLocal自己提供了这个问题的解决方案。

每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。

在这里插入图片描述

3. 使用static修饰ThreadLocal

还可以使用 static 修饰ThreadLocal,保证ThreadLocal为强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

原因:根据可达性算法分析,类静态属性引用的对象可作为GC Roots根节点,即保证了ThreadLocal为不可回收对象


❤最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以==一键三连哦!==,感谢支持,我们下次再见~~~


一键三连.png