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

本文涉及的产品
可观测监控 Prometheus 版,每月50GB免费额度
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 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后也就可以保证了我们同一线程在事务内的所有操作获取到的连接是同一个连接,也就保证了事务的原子性了。

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
3月前
|
JavaScript 前端开发
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
这篇文章主要讨论了axios的使用、原理以及源码分析。 文章中首先回顾了axios的基本用法,包括发送请求、请求拦截器和响应拦截器的使用,以及如何取消请求。接着,作者实现了一个简易版的axios,包括构造函数、请求方法、拦截器的实现等。最后,文章对axios的源码进行了分析,包括目录结构、核心文件axios.js的内容,以及axios实例化过程中的配置合并、拦截器的使用等。
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
|
3月前
|
JavaScript 前端开发
【Vue面试题二十七】、你了解axios的原理吗?有看过它的源码吗?
文章讨论了Vue项目目录结构的设计原则和实践,强调了项目结构清晰的重要性,提出了包括语义一致性、单一入口/出口、就近原则、公共文件的绝对路径引用等原则,并展示了单页面和多页面Vue项目的目录结构示例。
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
391 37
|
2月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题
面试官: 请你手写一份 Call()源码,看完此篇不用担心!
面试官: 请你手写一份 Call()源码,看完此篇不用担心!
|
3月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
4月前
|
存储 安全 Java
Android面试题之ArrayList源码详解
ArrayList是Java中基于数组实现的列表,提供O(1)的索引访问,但插入和删除操作平均时间复杂度为O(n)。默认容量为10,当需要时会通过System.arraycopy扩容。允许存储null,非线程安全。面试常问:List是接口,ArrayList是其实现之一,推荐使用List接口编程以实现更好的灵活性。更多详情见[ArrayList源码](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/ArrayList.java#ArrayList.Node)。
33 2
|
4月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
51 0
|
4月前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
42 0