带你整理面试过程中关于ThreadLocal的相关知识

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于ThreadLocal的相关知识相关的知识,希望对你有一定的参考价值。

文章目录

一、什么是 ThreadLocal

ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal, 每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的,主要用来解决线程内资源的共享,

比如,让100个人填写个人信息表,如果只有一支笔,那么大家就得挨个填写,对于管理人员来说,必须保证大家不会去哄抢这仅存的一支笔,否则,谁也填不完。
从另外一个角度出发,我们可以准备100支笔,人手一支,那么所有人很快就能完成表格的填写工作。如果说锁使用的是第一种思路,那么 ThreadLocal 使用的就是第二种思路。

锁就是通过控制资源的访问,而ThreadLocal是 通过增加资源来保证所有对象的线程安全

这也被叫做数据隔离,保证只有在线程内才能获取到对应的值,线程外不能访问。

线程的隔离特性:

  • Synchronized 是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal 是通过每个线程单独一份存储空间,牺牲空间来解决冲突

二、ThreadLocal 的应用场景

实际开发中我们真正使用ThreadLocal的场景还是比较少的,大多数使用都是在框架里面。

比如 Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接

日常使用如果某些数据是以线程为作用域并且不同线程有不同的数据副本时,可以考虑 ThreadLocal,比如 SimpleDateFormat 解决线程不安全的问题

不过现在 java8 提供了 DateTimeFormatter 它是线程安全的

三、ThreadLocal 的底层

首先,它是一个数据结构,有点像 HashMap,可以保存 “key : value” 键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。

ThreadLocal<String> localName = new ThreadLocal();
localName.set("Yolo");
String name = localName.get();

在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值 Yolo,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。

这是为什么,如何实现?不过之前也说了,ThreadLocal保证了各个线程的数据互不干扰。

1. set 和 get 方法

看看set(T value)get()方法的源码

 public void set(T value) 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
   	    //key为ThreadLocal当前对象,value就是我们需要的值
        map.set(this, value);
    else
        createMap(t, value);

//getMap方法
ThreadLocalMap getMap(Thread t) 
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 
//createMap
void createMap(Thread t, T firstValue) 
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);

可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取

而设置到ThreadLocal中的数据,也正是写入了threadLocals的这个Map。其中,key为ThreadLocal当前对象,value就是我们需要的值。
而 threadLocals 本身就保存了当前自己所在线程的所有“局部变量”,也就是一个 ThreadLocal 变量的集合。

所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。

在进行get()方法操作时,自然就是将这个Map中的数据拿出来。

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

get()方法先取得当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

2. ThreadLocalMap

ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的

ThreadLocalMap map = getMap(t);
ThreadLocalMap getMap(Thread t) 
        return t.threadLocals;
    

这也是ThreadLocal 数据隔离的真相,每个线程Thread都维护了自己的 threadLocals 变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程ThreadthreadLocals变量里面的,别人没办法拿到,从而实现了隔离

【基于1.7的源码】
ThreadLocalMap 既然有个Map那他的数据结构其实是很像 HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

static class ThreadLocalMap 

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

            Entry(ThreadLocal<?> k, Object v) 
                super(k);
                value = v;
            
        
        ……
      

ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

(1)为什么需要数组呢?

用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

(2)没有了链表怎么解决Hash冲突呢?

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

从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置iint i = key.threadLocalHashCode & (len-1)

① 如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) 
    replaceStaleEntry(key, value, i);
    return;

② 如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

if (k == key) 
    e.value = value;
    return;

③ 如果位置 i 的位置不为空,也就是发生了哈希碰撞,它不会像HashMap 变成链表或红黑树,而是使用拉链法进行存储。也就是同一个下标位置发生冲突时,则 +1 向后寻址 ,直到找到空位置或垃圾回收位置进行存储。

3. 内存泄露

ThreadLocal 可能导致内存泄漏

看下 Entry 的实现:

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

    Entry(ThreadLocal<?> k, Object v) 
        super(k);
        value = v;
    

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是keyvalue都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

弱引用:只要发生垃圾回收一定会被回收

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收(也就是 key 被回收了),如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。


如何避免内存泄露:

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocalget()set()可能会清除ThreadLocalMapkeynullEntry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocalset方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try 
    localName.set("张三");
    ……
 finally 
    localName.remove();

key 为什么要设计成弱引用 ?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。

【参考】

【1】https://www.cnblogs.com/aobing/p/13382184.html
【2】https://my.oschina.net/u/4477286/blog/3434394
【3】https://www.jianshu.com/p/377bb840802f
【4】https://blog.csdn.net/nanhuaibeian/article/details/112498514
【5】https://segmentfault.com/a/1190000024438006

以上是关于带你整理面试过程中关于ThreadLocal的相关知识的主要内容,如果未能解决你的问题,请参考以下文章

带你整理面试过程中关于锁的相关知识点下

带你整理面试过程中关于 Java 中的 异常分类及处理的相关知识

带你整理面试过程中关于 Mybatis 底层的相关知识

带你整理面试过程中关于多线程中的线程池的相关知识点

带你整理面试过程中关于Redis 中的字典及 rehash的相关知识点

带你整理面试过程中关于Innodb的相关知识点