多线程系列(七) -ThreadLocal 用法及内存泄露分析

news2024/12/24 0:17:43

一、简介

在 Java web 项目中,想必很多的同学对ThreadLocal这个类并不陌生,它最常用的应用场景就是用来做对象的跨层传递,避免多次传递,打破层次之间的约束。

比如下面这个HttpServletRequest参数传递的简单例子!

public class RequestLocal {

    /**
     * 线程本地变量
     */
    private static ThreadLocal<HttpServletRequest> local = new ThreadLocal<>();

    /**
     * 存储请求对象
     * @param request
     */
    public static void set(HttpServletRequest request){
        local.set(request);
    }

    /**
     * 获取请求对象
     * @return
     */
    public static HttpServletRequest get(){
        return local.get();
    }

    /**
     * 移除请求对象
     * @return
     */
    public static void remove(){
        local.remove();
    }
}

public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 存储请求对象变量
        RequestLocal.set(req);
        try {

            // 业务逻辑...
        } finally {
            // 请求完毕之后,移除请求对象变量
            RequestLocal.remove();
        }
    }
}
// 在需要的地方,通过 RequestLocal 类获取HttpServletRequest对象
HttpServletRequest request = RequestLocal.get();

看完以上示例,相信大家对ThreadLocal的使用已经有了大致的认识。

当然ThreadLocal的作用还不仅限如此,作为 Java 多线程模块的一部分,ThreadLocal也经常被一些面试官作为知识点用来提问,因此只有理解透彻了,回答才能更加游刃有余。

下面我们从ThreadLocal类的源码解析到使用方式做一次总结,如果有不正之处,请多多谅解,并欢迎批评指出。

二、源码剖析

ThreadLocal类,也经常被叫做线程本地变量,也有的叫做本地线程变量,意思其实差不多,通俗的解释:ThreadLocal作用是为变量在每个线程中创建一个副本,这样每个线程就可以访问自己内部的副本变量;同时,该变量对其他线程而言是封闭且隔离的。

字面的意思很容易理解,但是实际上ThreadLocal类的实现原理还有点复杂。

打开ThreadLocal类,它总共有 4 个public方法,内容如下!

方法描述
public void set(T value)设置当前线程变量
public T get()获取当前线程变量
public void remove()移除当前线程设置的变量
public static ThreadLocal withInitial(Supplier supplier)自定义初始化当前线程的默认值

其中使用最多的就是set()get()remove()方法,至于withInitial()方法,一般在ThreadLocal对象初始化的时候,给定一个默认值,例如下面这个例子!

// 给所有线程初始化一个变量 1
private static ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 1);

下面我们重点来剖析以上三个方法的源码,最后总结如何正确的使用。

以下源码解析均基于jdk1.8

2.1、set 方法

打开ThreadLocal类,set()方法的源码如下!

public void set(T value) {
    // 首先获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取当前线程中的变量 ThreadLocal.ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果不为空,就设置值
    if (map != null)
        map.set(this, value);
    else
        // 如果为空,初始化一个ThreadLocalMap变量,其中key为当前的threadlocal变量
        createMap(t, value);
}

我们继续看看createMap()方法的源码,内容如下!

void createMap(Thread t, T firstValue) {
    // 初始化一个 ThreadLocalMap 对象,并赋予给 Thread 对象
    // 可以发现,其实 ThreadLocalMap 是 Thread 类的一个属性变量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // INITIAL_CAPACITY 变量的初始值为 16
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

从上面的源码上你会发现,通过ThreadLocal类设置的变量,最终保存在每个线程自己的ThreadLocal.ThreadLocalMap对象中,其中key是当前线程的ThreadLocal变量,value就是我们设置的变量。

基于这点,可以得出一个结论:每个线程设置的变量只有自己可见,其它线程无法访问,因为这个变量是线程自己独有的属性

从上面的源码也可以看出,真正负责存储value变量的是Entry静态类,并且这个类继承了一个WeakReference类。稍有不同的是,Entry静态类中的key是一个弱引用类型对象,而value是一个强引用类型对象。这样设计的好处在于,弱引用的对象更容易被 GC 回收,当ThreadLocal对象不再被其他对象使用时,可以被垃圾回收器自动回收,避免可能的内存泄漏。关于这一点,我们在下文再详细的介绍。

最后我们再来看看map.set(this, value)这个方法的源码逻辑,内容如下!

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();
        // 如果key相同,直接进行覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果key为空,说明key被回收了,重新覆盖
        if (k == null) {
            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);
}

从上面的源码分析可以看出,ThreadLocalMapHashMap,虽然都是键值对的方式存储数据,当在数组中存储数据的下表冲突时,存储数据的方式有很大的不同。jdk1.8种的HashMap采用的是链表法和红黑树来解决下表冲突,当

ThreadLocalMap采用的是开放寻址法来解决hash冲突,简单的说就是当hash出来的存储位置相同但key不一样时,会继续寻找下一个存储位置,直到找到空位来存储数据。

jdk1.7中的HashMap采用的是链表法来解决hash冲突,当hash出来的存储位置相同但key不一样时,会将变量通过链表的方式挂在数组节点上。

为了实现更高的读写效率,jdk1.8中的HashMap就更为复杂了,当冲突的链表长度超过 8 时,链表会转变成红黑树,在此不做过多的讲解,有兴趣的同学可以翻看关于HashMap的源码分析文章。

一路分析下来,是不是感觉set()方法还是挺复杂的,总结下来set()大致的逻辑有以下几个步骤:

  • 1.首先获取当前线程对象,检查当前线程中的ThreadLocalMap是否存在
  • 2.如果不存在,就给线程创建一个ThreadLocal.ThreadLocalMap对象
  • 3.如果存在,就设置值,存储过程中如果存在 hash 冲突时,采用开放寻址法,重新找一个空位进行存储
2.2、get 方法

了解完set()方法之后,get()方法就更容易了,get()方法的源码如下!

public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 从当前线程对象中获取 ThreadLocalMap 对象
    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();
}

这里我们要重点看下map.getEntry(this)这个方法,源码如下!

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果找到key,直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 如果找不到,就尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // e指的是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        // 如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            // 如果key为null,说明已经被回收了,同时将value设置为null,以便进行回收
            expungeStaleEntry(i);
        else
            // 如果key不是要找的那个,那说明有hash冲突,继续找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

从上面的源码可以看出,get()方法逻辑,总共有以下几个步骤:

  • 1.首先获取当前线程对象,从当前线程对象中获取 ThreadLocalMap 对象
  • 2.然后判断ThreadLocalMap是否存在,如果存在,就尝试去获取最终的value
  • 3.如果不存在,就重新初始化默认值,以便清理旧的value

其中expungeStaleEntry()方法是真正用于清理value值的,setInitialValue()方法也具备清理旧的value变量作用。

从上面的代码可以看出,ThreadLocal为了清楚value变量,花了不少的心思,其实本质都是为了防止ThreadLocal出现可能的内存泄漏。

2.3、remove 方法

我们再来看看remove()方法,源码如下!

public void remove() {
    // 获取当前线程里面的 ThreadLocalMap 对象
    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);
    // 循环遍历目标key,然后将key和value都设置为null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            // 清理value值
            expungeStaleEntry(i);
            return;
        }
    }
}

remove()方法逻辑比较简单,首先获取当前线程的ThreadLocalMap对象,然后循环遍历key,将目标key以及对应的value都设置为null

从以上的源码剖析中,可以得出一个结论:不管是set()get()还是remove(),其实都会主动清理无效的value数据,因此实际开发过程中,没有必要过于担心内存泄漏的问题。

三、为什么要用 WeakReference?

另外细心的同学可能会发现,ThreadLocal中真正负责存储keyvalue变量的是Entry静态类,并且它继承了一个WeakReference类。

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

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

关于WeakReference类,我们在上文只是简单的说了一下,可能有的同学不太清楚,这个再次简要的介绍一下。

了解过WeakHashMap类的同学,可能对WeakReference有印象,它表示当前对象为弱引用类型

在 Java 中,对象有四种引用类型,分别是:强引用、软引用、弱引用和虚引用,级别从高依次到低。

不同引用类型的对象,GC 回收的方式也不一样,对于强引用类型,不会被垃圾收集器回收,即使当内存不足时,另可抛异常也不会主动回收,防止程序出现异常,通常我们自定义的类,初始化的对象都是强引用类型;对于软引用类型的对象,当不存在外部强引用的时候,GC 会在内存不足的时候,进行回收;对于弱引用类型的对象,当不存在外部强引用的时候,GC 扫描到时会进行回收;对于虚引用,GC 会在任何时候都可能进行回收。

下面我们看一个简单的示例,更容易直观的了解它。

public static void main(String[] args) {
        Map weakHashMap = new WeakHashMap();
        //向weakHashMap中添加4个元素
        for (int i = 0; i < 3; i++) {
            weakHashMap.put("key-"+i, "value-"+ i);
        }
        //输出添加的元素
        System.out.println("数组长度:"+weakHashMap.size() + ",输出结果:" + weakHashMap);
        //主动触发一次GC
        System.gc();
        //再输出添加的元素
        System.out.println("数组长度:"+weakHashMap.size() + ",输出结果:" + weakHashMap);
    }

输出结果:

数组长度:3,输出结果:{key-2=value-2, key-1=value-1, key-0=value-0}
数组长度:3,输出结果:{}

以上存储的弱引用对象,与外部对象没有强关联,当主动调用 GC 回收器的时候,再次查询WeakHashMap里面的数据的时候,弱引用对象收回,所以内容为空。其中WeakHashMap类底层使用的数据存储对象,也是继承了WeakReference

采用WeakReference这种弱引用的方式,当不存在外部强引用的时候,就会被垃圾收集器自动回收掉,减小内存空间压力。

需要注意的是,Entry静态类中仅仅只是key被设计成弱引用类型,value依然是强引用类型。

回归正题,为什么ThreadLocalMap类中的Entry静态类中的key需要被设计成弱引用类型?

我们先看一张Entry对象的依赖图!

如上图所示,Entry持有ThreadLocal对象的引用,如果没有设置引用类型,这个引用链就全是强引用,当线程没有结束时,它持有的强引用,包括递归下去的所有强引用都不会被垃圾回收器回收;只有当线程生命周期结束时,才会被回收。

哪怕显式的设置threadLocal = null,它也无法被垃圾收集器回收,因为Entrykey存在强关联!

如果Entry中的key设置成弱引用,当threadLocal = null时,key就可以被垃圾收集器回收,进一步减少内存使用空间。

但是也仅仅只是回收key,不能回收value,如果这个线程运行时间非常长,又没有调用set()get()或者remove()方法,随着线程数的增多可能会有内存溢出的风险。

因此在实际的使用中,想要彻底回收value,使用完之后可以显式调用一下remove()方法。

四、应用介绍

通过以上的源码分析,相信大家对ThreadLocal类已经有了一些认识,它主要的作用是在线程内实现变量的传递,每个线程只能看到自己设定的变量。

我们可以看一个简单的示例!

public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    threadLocal.set("main");

    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置变量
                threadLocal.set(String.valueOf(j));
                // 获取设置的变量
                System.out.println("thread name:" + Thread.currentThread().getName() + ", 内容:" + threadLocal.get());
            }
        }).start();
    }
    
    System.out.println("thread name:" + Thread.currentThread().getName() + ", 内容:" + threadLocal.get());
}

输出结果:

thread name:Thread-0, 内容:0
thread name:Thread-1, 内容:1
thread name:Thread-2, 内容:2
thread name:Thread-3, 内容:3
thread name:main, 内容:main
thread name:Thread-4, 内容:4

从运行结果上可以很清晰的看出,每个线程只能看到自己设置的变量,其它线程不可见。

ThreadLocal可以实现线程之间的数据隔离,在实际的业务开发中,使用非常广泛,例如文章开头介绍的HttpServletRequest参数的上下文传递。

五、小结

最后我们来总结一下,ThreadLocal类经常被叫做线程本地变量,它确保每个线程的ThreadLocal变量都是各自独立的,其它线程无法访问,实现线程之间数据隔离的效果。

ThreadLocal适合在一个线程的处理流程中实现参数上下文的传递,避免同一个参数在所有的方法中传递。

使用ThreadLocal时,如果当前线程中的变量已经使用完毕并且永久不在使用,推荐手动调用移除remove()方法,可以采用try ... finally结构,并在finally中清除变量,防止存在潜在的内存溢出风险。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1649972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习项目实践-基础知识部分

环境建立 我们做项目第一步就是单独创建一个python环境&#xff0c;Python新的隔离环境 创建&#xff1a;python -m venv ml 使用&#xff1a;.\Scripts\activate python -m venv ml 是在创建一个名为 ml 的虚拟环境&#xff0c;这样系统会自动创建一个文件夹ml&#xff0c;…

[redis] 说一说 redis 的底层数据结构

Redis有动态字符串(sds)、链表(list)、字典(ht)、跳跃表(skiplist)、整数集合(intset)、压缩列表(ziplist) 等底层数据结构。 Redis并没有使用这些数据结构来直接实现键值对数据库&#xff0c;而是基于这些数据结构创建了一个对象系统&#xff0c;来表示所有的key-value。 文章…

鸿蒙编译子系统详解(二)main.py

1.5.4源码解析 1.5.4.1 build/hb/main.py脚本 这个脚本是编译的主程序脚本&#xff0c;流程如下&#xff1a; 首先是初始化各种module类&#xff0c;然后运行对应模块。 hb分为build,set,env,clean,tool,help几个模块&#xff0c;模块源码位于build/hb/modules/目录下&#xff…

安卓开发(二)Android开发基础知识

了解Android Android大致可以分为4层架构&#xff1a;Linux内核层、系统运行库层、应用框架层和应用层。 内核层&#xff1a;Android系统是基于Linux内核的&#xff0c;这一层为Android设备的各种硬件提供了底层的驱动&#xff0c;如显示驱动、音频驱动、照相机驱动、蓝牙驱动…

MS2107 宏晶微 音视频采集芯片 提供开发资料

1. 基本介绍 MS2107 是一款视频和音频采集芯片,内部集成 USB2.0 控制器和数据收发模块、视频 ADC模块、音频 ADC 模块和音视频处理模块。MS2107可以将 CVBS、S-Video 和音频信号通过 USB接口传送到 PC、智能手机和平板电脑上预览或采集。MS2107 输出支持 YUV422 和 MJPEG 两种…

Python基础详解二

一&#xff0c;函数 函数是组织好的&#xff0c;可重复使用的&#xff0c;用来实现某个功能的代码段 def myMethod(data):print("数据长度为",len(data))myMethod("dsdsdsds") 函数的定义&#xff1a; def 函数名(传入参数):函数体return 返回值 def m…

MahApps.Metro的MVVM模式介绍(一)

MahApps.Metro是一个开源的WPF (Windows Presentation Foundation) UI 控件库。它的特点有现代化设计、主题定制、响应式布局、内置控件。 而Mvvm模式的核心思想是将用户界面&#xff08;View&#xff09;与应用程序逻辑&#xff08;ViewModel&#xff09;分离&#xff0c;以实…

软件架构的艺术:探索演化之路上的18大黄金原则

实际工作表明&#xff0c;一步到位的设计往往不切实际&#xff0c;而演化原则指导我们逐步优化架构&#xff0c;以灵活响应业务和技术的变化。这不仅降低了技术债务和重构风险&#xff0c;还确保了软件的稳定性和可扩展性。同时&#xff0c;架构的持续演进促进了团队协作&#…

C语言例题35、反向输出字符串(指针方式),例如:输入abcde,输出edcba

#include <stdio.h>void reverse(char *p) {int len 0;while (*p ! \0) { //取得字符串长度p;len;}while (len > 0) { //反向打印到终端printf("%c", *--p);len--;} }int main() {char s[255];printf("请输入一个字符串&#xff1a;");gets(s)…

MIT加州理工等革命性KAN破记录,发现数学定理碾压DeepMind!KAN论文解读

KAN的数学原理 如果f是有界域上的多元连续函数&#xff0c;那么f可以被写成关于单个变量和加法二元操作的连续函数的有限组合。更具体地说&#xff0c;对于光滑函数f&#xff1a;[0, 1]ⁿ → R&#xff0c;有 f ( x ) f ( x 1 , … , x n ) ∑ q 1 2 n 1 Φ q ∑ p 1 n …

web 基础之 HTTP 请求

web 基础 网上冲浪 就是在互联网(internet)上获取各种信息&#xff0c;进行工作&#xff0c;或者娱乐&#xff0c;他的英文表示surfing the Internet&#xff0c;因 “surfing”d的意思是冲浪&#xff0c;即成为网上冲浪&#xff0c;这是一种形象说法&#xff0c; 也是一个非…

数据交换和异步请求(JSONAjax))

目录 一.JSON介绍1.JSON的特点2.JSON的结构3.JSON的值JSON示例4.JSON与字符串对象转换5.注意事项 二.JSON在Java中的使用1.Javabean to json2.List to json3.Map to JSONTypeToken底层解析 三.Ajax介绍1.介绍2.Ajax经典应用场景 四.Ajax原理示意图1. 传统web应用2.Ajax方法 五.…

贪吃蛇大作战(C语言--实战项目)

朋友们&#xff01;好久不见。经过一段时间的沉淀&#xff0c;我这篇文章来和大家分享贪吃蛇大作战这个游戏是怎么实现的。 &#xff08;一&#xff09;.贪吃蛇背景了解及效果展示 首先相信贪吃蛇游戏绝对称的上是我们00后的童年&#xff0c;不仅是贪吃蛇还有俄罗斯⽅块&…

YAML如何操作Kubernetes核心对象

Pod Kubernetes 最核心对象Pod Pod 是对容器的“打包”&#xff0c;里面的容器&#xff08;多个容器&#xff09;是一个整体&#xff0c;总是能够一起调度、一起运行&#xff0c;绝不会出现分离的情况&#xff0c;而且 Pod 属于 Kubernetes&#xff0c;可以在不触碰下层容器的…

Day 63:单调栈 LeedCode 84.柱状图中最大的矩形

84. 柱状图中最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;10 解释&a…

Linux系统使用Docker安装青龙面板并实现远程访问管理面板

文章目录 一、前期准备本教程环境为&#xff1a;Centos7&#xff0c;可以跑Docker的系统都可以使用。本教程使用Docker部署青龙&#xff0c;如何安装Docker详见&#xff1a; 二、安装青龙面板三、映射本地部署的青龙面板至公网四、使用固定公网地址访问本地部署的青龙面板 青龙…

Codeforces Round 942 (Div. 2) A-D1

题目&#xff1a; Codeforces Round 942 (Div. 2) D2有缘再补吧… A. Contest Proposal 题意 两个升序&#xff08;不降&#xff09;的序列a和b&#xff0c;可以在a的任意位置插入任意数&#xff08;要保持升序&#xff09;&#xff0c;使对任意i&#xff0c;有a[i] < b[…

ENVI下实现遥感矿物蚀变信息提取

蚀变岩石是在热液作用影响下&#xff0c;使矿物成分、化学成分、结构、构造等发生变化的岩石。由于它们经常见于热液矿床的周围&#xff0c;因此被称为蚀变围岩&#xff0c;蚀变围岩是一种重要的找矿标志。利用围岩蚀变现象作为找矿标志已有数百年历史&#xff0c;发现的大型金…

ldap对接jenkins

ldap结构 配置 - jenkins进入到 系统管理–>全局安全配置 - 安全域 选择ldap - 配置ldap服务器地址&#xff0c;和配置ldap顶层唯一标识名 配置用户搜索路径 - 配置管理员DN和密码 测试认证是否OK

Java | Leetcode Java题解之58题最后一个单词的长度

题目&#xff1a; 题解&#xff1a; class Solution {public int lengthOfLastWord(String s) {int index s.length() - 1;while (s.charAt(index) ) {index--;}int wordLength 0;while (index > 0 && s.charAt(index) ! ) {wordLength;index--;}return wordL…