常见面试题梳理:源码角度彻底揭秘ThreadLocal

简介: ThreadLocal在日常开发中还是比较常见的,本文将从源码的角度彻底揭秘ThreadLocal,并会分享一些较为常见的面试题,let's go。ThreadLocal是什么?ThreadLocal隶属于lang包,它的主要功能是为每个线程提供一个私有的局部变量,这个变量在线程间相互隔离,互不影响。主要解决的就是单例情况下全局变量的线程安全问题

序言

ThreadLocal在日常开发中还是比较常见的,本文将从源码的角度彻底揭秘ThreadLocal,并会分享一些较为常见的面试题,let's go。

ThreadLocal是什么?

ThreadLocal隶属于lang包,它的主要功能是为每个线程提供一个私有的局部变量,这个变量在线程间相互隔离,互不影响。

主要解决的就是单例情况下全局变量的线程安全问题

ThreadLocal的底层实现

set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
复制代码
  • 通过set方法可得知,先获取到当前的Thread对象,然后调用getMap(t)方法
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
 }
复制代码
  • getMap方法的内部实现也很简单,直接调用t的threadlocals字段,来获取到当前线程对应的ThreadLocalMap对象
  • 接下来会判断map是否存在,不存在的话就去创建出map,存在的话就调用map.set(this,value)方法了
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();
     }
复制代码
  • 以当前的ThreadLocal作为key,set的值作为value,然后封装成entry对象放到ThreadLocalMap当中。

当发生hash冲突时,采用的解决方式是线性探测法来解决的。

set方法小总结

通过set方法的阅读,我们基本可以得出以下结论:ThreadLocal本身不存放数据,而是通过Thread对应的ThreadLocalMap来存放数据,ThreadLocal只是作为key。

ThreadLocalMap

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
}
复制代码
  • ThreadLocalMap中通过一个内部类Entry来存放key,value,我们需要注意的是这个Entry对象是继承自WeakReference,WeakReference对象是一个弱引用对象(弱引用对象的特点是:当垃圾收集器进行gc时,如果没有引用指向弱引用对象的话,那么就会进行回收)

弱引用仅限于Key,value还是强引用对象

为什么key要设为弱引用?

我个人认为,key设为弱引用,是为了方便当ThreadLocal对象使用完毕后将key进行垃圾回收,避免出现内存泄漏。

key是不内存泄漏了,但是value还是会出现内存泄漏。(过会儿我们仔细说一下value的内存泄漏问题)

get方法

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方法的主要作用就是获取我们set进去的值,先获取到threadLocalMap对象,然后将当前的threadLocal对象引用作为key从threadLocalMap中获取到对应的value。

从源码中可知,我们需要注意的是,有可能在操作threadLocal对象时,没有先执行set()方法,直接调用get()方法,那么它会返回setInitialValue()方法,我们一起来看看setInitialValue()做了什么。

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
复制代码
  • 第一行调用了initialValue()方法,我们先按下表,看看initialValue()做了什么
protected T initialValue() {
        return null;
    }
复制代码

initialValue()方法直接返回了null

  • 回到setInitialValue()方法,我们可以知道第一行代码T value = initialValue()执行完后,value是null,接下来的代码相信大家已经不陌生了
  • 继续获取到当前Thread对象,然后获取到ThreadLocalMap对象,然后以ThreadLocal对象作为key,null作为value写入到ThreadLocalMap当中。
  • 然后返回null

get方法小总结

会以当前的ThreadLocal作为key,从ThreadLocalMap中获取到set的value。

如果我们没有调用set,而直接调用get的话,默认情况下会返回null(并帮我们调用set方法,value就设为null)

initialValue方法

从上面我们可以知道,默认情况下initialValue方法是返回null的,其实我们在新建ThreadLocal对象时可以重写initialValue方法。

ThreadLocal<String> threadLocal=new ThreadLocal<String>(){
           @Override
           protected String initialValue() {
              return "haha";
          }
   };
  System.out.println(threadLocal.get());
复制代码

我们重写了initialValue方法,这样在直接get时就会获取到我们写的“haha”了,运行结果如下:

网络异常,图片无法展示
|

remove方法

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

remove方法一般在用完ThreadLocal后进行调用,它的主要作用就是清除掉当前ThreadLocal对象在ThreadLocalMap中对应的entry。

ThreadLocal底层图

网络异常,图片无法展示
|

ThreadLocal的内存泄漏问题

内存泄漏:内存泄漏就是指我们使用完毕的资源,没有得到及时的释放,jvm还认为该资源有用,不会对其进行回收,导致该资源一直占用着我们的内存,最终很有可能导致内存溢出。

ThreadLocal的内存泄漏:上面我们提到ThreadLocalMap的key设为弱引用是为了解决key内存泄漏的问题,但value依旧是会有内存泄漏问题存在的。

当我们使用完ThreadLocal后,垃圾回收器会将key给回收掉,但是value却是一直存在的,直到线程结束才会释放,但我们日常开发中会有使用线程池的场景,在这个场景下线程的生命周期都是较长的,这个时间段内就造成了value的内存泄漏,因此ThreadLocal的内存泄漏和key是不是弱引用关系不大,主要还是由于使用完后没有调用remove()方法造成的。

为了避免内存泄漏,我们最好在使用完ThreadLocal后,调用其remove()方法。

ThreadLocal在Spring中的应用

Spring框架相信大家都不陌生,Spring框架中有一个@Transactional注解,它是用于保证事务的。

事务的主要作用就是保证同一事务下的操作要么全部成功,要么全部失败,但有一个前提条件就是这些操作必须使用同一个数据库连接,但是数据库连接不是线程安全的,它在多线程环境下会出现问题。

Spring为了保证事务的原子性,它就采用了ThreadLocal这个数据结构,用ThreadLocal来保存连接,set的类型是一个Map,key是数据源、value是连接,定义成map是为了应对多数据源的场景的,当采用了ThreadLocal后也就可以保证了我们同一线程在事务内的所有操作获取到的连接是同一个连接,也就保证了事务的原子性了。

相关文章
|
1月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
36 0
|
5月前
|
存储 Java 中间件
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
|
8月前
|
分布式计算 Hadoop 大数据
字节跳动面试问到Hadoop源码,拿40K进大厂的Java程序员必备技能
大数据由于数据量庞大、数据类型复杂等特点,特别是非结构化或半结构化数据远远多于结构化数据,导致传统关系型数据库让企业面临巨大的成本压力。而 Hadoop 能够很好的提供解决大数据问题的技术手段。因此,Hadoop 技术在大数据中占据着举足轻重的作用,也很受企业的青睐。
|
2天前
|
存储 Java
面试高频 ThreadLocal类详解
面试高频 ThreadLocal类详解
5 0
|
17天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
42 1
|
5月前
|
存储 算法 Java
[Java 源码] 秋招常被问到 GC 相关的几道面试题(集中在分配以及回收)
[Java 源码] 秋招常被问到 GC 相关的几道面试题(集中在分配以及回收)
|
5月前
|
分布式计算 Hadoop 大数据
字节跳动面试问到Hadoop源码,拿40K进大厂的Java程序员必备技能
大数据由于数据量庞大、数据类型复杂等特点,特别是非结构化或半结构化数据远远多于结构化数据,导致传统关系型数据库让企业面临巨大的成本压力。而 Hadoop 能够很好的提供解决大数据问题的技术手段。因此,Hadoop 技术在大数据中占据着举足轻重的作用,也很受企业的青睐。
|
5月前
|
缓存 算法 Java
堪称神级的阿里巴巴“高并发”教程《基础+实战+源码+面试+架构》
作为一个普普通通的程序员,如何才能提升自己的能力,在职场上拥有一技之长,这也成为普通的你我,迫切的需求。
|
6月前
|
设计模式 Dubbo NoSQL
6年Java经验,4面阿里定级P7,多亏阿里13万字+脑图+源码面试笔记
互联网公司的面试,从形式上说,一般分为 2~3轮技术面 +1 轮 HR 面,不过某些公司没有 HR 面试。 在技术面试中,面试官一般会就你所应聘的岗位进行相关知识的考察,也叫基础知识和业务逻辑面试,只要回答的不是特别差,面试官通常会继续跟你聊聊项目,这个时候就开始了项目面试。
|
6月前
|
负载均衡 前端开发 Java
阿里面试:看过框架源码吗?举例说明一下
阿里面试:看过框架源码吗?举例说明一下
83 0