并发编程模式之ThreadLocal源码和图文解读

 更新时间:2024年08月13日 09:31:44   作者:it_lihongmin  
这篇文章主要介绍了并发编程模式之ThreadLocal源码和图文解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

从一道面试题开始吧,ThreadLocal使用需要注意什么,或者有什么问题?

  • 答:如果是在线程池中使用,会存在 1、内存泄漏 2、脏数据 的问题
  • 解决:try finally中调用 remove方法

慢慢从ThreadLocal的设计和源码开始分析,

一、ThreadLocal结构和存取数据

当创建了一个ThreadLocal对象时,可以将存放的Object对象、或者回调函数(延迟加载)放入setInitialValue中;当当前线程去获取时,则会在当前线程Thread的属性threadLocals中获取,而该属性的类型则为TheadLocal的静态内部类ThreadLocalMap,但是引用还是指向了Thread;而该threadLocals的ThreadLocalMap内部维护了一个其内部类ThreadLocal.ThreadLocalMap.Entry数组,而Entry由key和value组成,key即为当前的 new 的ThreadLocal对象本身,value为我们当前存储的Object对象,并且key为WeakReference(弱引用)类型。

所以,ThreadLocal本身只定义了一些内部类,而并不真实拥有任何数据,可以理解为全是空壳子;当获取数据时若当前线程的ThreadLocal对象(内存地址)不存在,则才会在该对象的setInitialValue中copy一份值到Thread的属性threadLocals中,key为当前对象引用,value为存储的Object值;当创建了多个ThreadLocal对象时,当每当前线程都调用过ThreadLocal进行getset时,则会在当前线程的threadLocals中存储多个值,

如下图:

1、ThreadLocal#get

public T get() {
    Thread var1 = Thread.currentThread();
    ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
    if (var2 != null) {
        ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
        if (var3 != null) {
            Object var4 = var3.value;
            return var4;
        }
    }
 
    return this.setInitialValue();
}

获取当前的线程,并且使用当前线程去获取ThreadLocal.ThreadLocalMap类型的对象,先看看getMap方法,

ThreadLocal.ThreadLocalMap getMap(Thread var1) {
    return var1.threadLocals;
}

什么都没有做,只是将传入的当前对象的threadLocals属性返回,只是该引用还是指向了Thread本身。

继续,

1、如果threadLocals对象本身不为null,则通过this,即当前ThreadLocal对象的引用为key,获取对应Entry的值返回。

2、如果threadLocals对象本身为null,说明当前线程中没有存放过任何ThreadLocal对象的引入的值。

则需要调setInitialValue方法,如下:

private T setInitialValue() {
    Object var1 = this.initialValue();
    Thread var2 = Thread.currentThread();
    ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
    if (var3 != null) {
        var3.set(this, var1);
    } else {
        this.createMap(var2, var1);
    }
 
    return var1;
}

调用当前ThreadLocal对象的initialValue方法,如果我们没有重写,则当前默认返回null,否则调用我们重写的方法,可以理解为懒加载。

每个线程第一次都会调用该方法获取的返回值,那么如果没次调用都是返回同一个对象则Thread中存储的就是同一个对象,需要我们自己注意(如果同一对象线程A改变之后线程B也改变了),并且也存在线程安全的问题。

还是获取当前Map是否为null,则调用createMap方法。

如下:

void createMap(Thread var1, T var2) {
    var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
}

2、ThreadLocal#set

理解完get的过程,set的就比较容易了,主要是引用本身比较绕。

public void set(T var1) {
    Thread var2 = Thread.currentThread();
    ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
    if (var3 != null) {
        var3.set(this, var1);
    } else {
        this.createMap(var2, var1);
    }
 
}

获取到Thread的属性threadLocals,如果是null则new一个,否则就往Map中放一个Entry对象。

二、梳理存在的问题和解决

当在线程池中使用的时候,其实大部分情况下我们都会在线程池中使用,比如Tomcat线程池。则key为弱引用,在Root gc不可达的情况下,则会被JVM进行回收。但是正是因为如此value值将永远的游离,Root gc永远指向不到该value值,则不能进行gc。当频繁的调用,则这样的对象越来越多,发生内存泄漏。如果该对象的内存还比较大,则会照成后续分配内存时新生代不足,则可能照成溢出的情况(溢出的个人理解)。

并且,当循环使用线程的话,比如存放的是用户信息,如果上一个用户请求离开时未清除数据,下一个用户进来直接获取的话会拿到上一个用户的数据,如果是存储的积分等,则完全就是脏数据

一并解决上面的两个问题方法比较简单,每次在调用代码时增加 try finally中调用 ThreadLocal对象的remove方法,如下:

ThreadLocal#remove

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

首先还是获取当前的线程去获取 Thread的属性threadLocals调用其(ThreadLocalMap的)remove方法,如下:

private void remove(ThreadLocal<?> var1) {
    ThreadLocal.ThreadLocalMap.Entry[] var2 = this.table;
    int var3 = var2.length;
    int var4 = var1.threadLocalHashCode & var3 - 1;
 
    for(ThreadLocal.ThreadLocalMap.Entry var5 = var2[var4]; var5 != null; 
            var5 = var2[var4 = nextIndex(var4, var3)]) {
        if (var5.get() == var1) {
            var5.clear();
            this.expungeStaleEntry(var4);
            return;
        }
    }
}

根据当前的ThreadLocal引用,去Map中循环匹配,当匹配到之后,调用Entry的remove方法,其实是调用Refrenceclear方法。最后调用expungeStaleEntry方法将其对应的Entry从数组中消除。

再看看Referenceclear方法:

public void clear() {
    this.referent = null;
}

将该值直接置位null,则后续JVM会对该对象进行gc。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java实现多人聊天室可视化

    java实现多人聊天室可视化

    这篇文章主要为大家详细介绍了java实现多人聊天室可视化,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • Java 非静态初始化的例子

    Java 非静态初始化的例子

    非静态初始化和静态初始化一模一样,只不过少了static关键字。但是如果两者共存的话,非静态初始化是比静态初始化慢一拍的。下边我们举两个例子来看一下。
    2020-09-09
  • 使用Spring自带的缓存注解维护数据一致性的全过程

    使用Spring自带的缓存注解维护数据一致性的全过程

    Cache(缓存),已经是项目中不可缺失的存在,一旦项目中需要使用到缓存,就需要考虑到数据一致性问题,即缓存数据与数据库数据的一致性问题,本文介绍在 Spring Boot 项目中,如何使用 Spring 自带的注解来进行数据一致性的维护,需要的朋友可以参考下
    2026-01-01
  • java基于双向环形链表解决丢手帕问题的方法示例

    java基于双向环形链表解决丢手帕问题的方法示例

    这篇文章主要介绍了java基于双向环形链表解决丢手帕问题的方法,简单描述了丢手帕问题,并结合实例形式给出了Java基于双向环形链表解决丢手帕问题的步骤与相关操作技巧,需要的朋友可以参考下
    2017-11-11
  • MyBatis将查询出的两列数据装配成键值对的操作方法

    MyBatis将查询出的两列数据装配成键值对的操作方法

    这篇文章主要介绍了MyBatis将查询出的两列数据装配成键值对的操作代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • 深入理解java动态代理机制

    深入理解java动态代理机制

    本篇文章主要介绍了深入理解java动态代理机制,详细的介绍动态代理有哪些应用场景,什么是动态代理,怎样使用,它的局限性在什么地方?有兴趣的可以了解一下。
    2017-02-02
  • SpringCloud maven-assembly-plugin 多级目录打包的实现

    SpringCloud maven-assembly-plugin 多级目录打包的实现

    本文主要介绍了SpringCloud maven-assembly-plugin 多级目录打包的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • JAVA十大排序算法之快速排序详解

    JAVA十大排序算法之快速排序详解

    这篇文章主要介绍了java中的快速排序,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-08-08
  • Java实现权重随机算法详解

    Java实现权重随机算法详解

    平时,经常会遇到权重随机算法,从不同权重的N个元素中随机选择一个,并使得总体选择结果是按照权重分布的。本文就详细来介绍如何实现,感兴趣的可以了解一下
    2021-07-07
  • Java之JVM的内存分析(简单版+案例)

    Java之JVM的内存分析(简单版+案例)

    这篇文章主要介绍了Java之JVM的内存分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05

最新评论