哥,我还是不懂 ThreadLocal

2023年 11月 2日 52.3k 0

大家好,我是风筝

前几天群里有个弟弟说看 TheadLocal 有点懵,我就把之前写的那篇给他扔过去了,结果他看完了跟我说:哥,我还是没看懂啊!

什么,这意思就是我写的那篇文章不行啊,看完了也看不懂,这怎么能行。于是我问他现在纠结在哪里了,啥地方不懂。经过一番沟通,我发现那篇文章确实写得不太行,好多新手不理解的点都没有点出来。

具体的一些容易让人迷糊的点有以下几个,虽然有一些问题看起来很傻,但是它们确实存在。

  • ThreadLocal 存的值在不同线程间怎么传递?
  • ThreadLocal 以什么形式存储?
  • ThreadLocal 可不可以放多个值?
  • ThreadLocal 到底是存在哪?跟线程有什么关系?
  • 咱们上来先看一段代码精神精神,接下来再一一解释上面的问题。这段代码中声明了两个 ThreadLocal ,然后在线程0和线程1中分别赋值这两个 ThreadLocal,第三个线程不赋值,在每个线程中打印这两个 ThreadLocal 的值。

    看一下应该输出的值是多少。

    public static void main(String[] args) throws InterruptedException {  
        ThreadLocal threadLocal1 = ThreadLocal.withInitial(() -> "啥都没干,初始值");  
        ThreadLocal threadLocal2 = new ThreadLocal();  
        Thread thread0 = new Thread() {  
            @Override  
            public void run() {  
                threadLocal1.set("我是threadLocal1 「Thread0」给我赋的值");  
                threadLocal2.set("我是threadLocal2 「Thread0」给我赋的值");  
                String name = "Thread0-";  
                System.out.println(name + "threadLocal1 = " + threadLocal1.get());  
                System.out.println(name + "threadLocal2 = " + threadLocal2.get());  
            }  
        };  
        thread0.start();  
        thread0.join();  
        System.out.println();  
      
        Thread thread1 = new Thread() {  
            @Override  
            public void run() {  
                threadLocal1.set("我是threadLocal1 「Thread1」给我赋的值");  
                threadLocal2.set("我是threadLocal2 「Thread1」给我赋的值");  
                String name = "Thread1-";  
                System.out.println(name + "threadLocal1 = " + threadLocal1.get());  
                System.out.println(name + "threadLocal = " + threadLocal2.get());  
            }  
        };  
        thread1.start();  
        thread1.join();  
        System.out.println();  
      
        Thread thread2 = new Thread() {  
            @Override  
            public void run() {  
                String name = "Thread2-";  
                System.out.println(name + "threadLocal1 = " + threadLocal1.get());  
            }  
        };  
        thread2.start();  
    }

    下面是输出的值,看看是不是和你理解的一致。

    Thread0-threadLocal1 = 我是threadLocal1 「Thread0」给我赋的值 Thread0-threadLocal2 = 我是threadLocal2 「Thread0」给我赋的值

    Thread1-threadLocal1 = 我是threadLocal1 「Thread1」给我赋的值 Thread1-threadLocal = 我是threadLocal2 「Thread1」给我赋的值

    Thread2-threadLocal1 = 啥都没干,初始值

    如果和你想的输出是一样的,那你可能已经理解了 TheadLocal 了,如果不一致的话,那说明你还没有掌握它。

    问题1:ThreadLocal 存的值在不同线程间怎么传递?

    我听到这个问题有些诧异了,你真是一点儿都没懂啊。ThreadLocal 当然不需要在进程间传递了,ThreadLocal 的初衷就是为了不在进程间传递值,而只是在当前线程的各个地方都能获取到。

    这就要说到 ThreadLocal 的定义和应用场景了。

    ThreadLocal 定义以及使用场景

    ThreadLocal允许每个线程独立存储和访问线程本地变量。线程本地变量是指每个线程都有自己独立的变量副本,互不干扰。这对于多线程编程来说非常有用,因为它允许在每个线程中存储状态或数据,而不需要担心线程间的竞争条件。

    我们进到 ThreadLocal 的源码中,通过源码注释就可以看到很清晰的解释:它是线程的局部变量,这些变量只能在这个线程内被读写,在其他线程内是无法访问的。ThreadLocal 定义的通常是与线程关联的私有静态字段(例如,用户ID或事务ID)。

    变量有局部的还有全局的,局部变量没什么好说的,一涉及到全局,那自然就会出现多线程的安全问题,要保证多线程安全访问,不出现脏读脏写,那就要涉及到线程同步了。而 ThreadLocal 相当于提供了介于局部变量与全局变量中间的这样一种线程内部的全局变量。

    根据 ThreadLocal 的定义,我们就可以知道它的使用场景了。就是当我们只想在本身的线程内使用的变量,比如这个线程要存活一段时间,可以用 ThreadLocal 来实现,并且这些变量是和线程的生命周期密切相关的,线程结束,变量也就销毁了。

    举几个例子说明一下:

    1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;

    2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统的分析一次 web 请求的过程:

    • 用户在浏览器中访问 web 页面;
    • 浏览器向服务器发起请求;
    • 服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
    • 最后服务器将请求结果返回给客户端浏览器。

    从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

    3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;

    4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

    使用方式

    ThreadLocal 的使用非常简单,最核心的操作就是四个:创建、创建并赋初始值、赋值、取值。

    1、创建

    ThreadLocal mLocal = new ThreadLocal();

    2、创建并赋初值。下面代码表示创建了一个 String 类型的 ThreadLocal 并且重写了 initialValue 方法,并返回初始字符串,之后调用 get() 方法获取的值便是 initialValue 方法返回的值。

    ThreadLocal mLocal = new ThreadLocal(){
                @Override
                protected String initialValue(){
                    return "init value";
                }
            };
    System.out.println(mLocal.get());

    3、设置值

    mLocal.set("hello");

    4、取值

    mLocal.get()

    实现原理

    前面回答了第一个问题,后面的三个问题就涉及到 ThreadLocal 的原理了。

    首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象,所以你可以在 ThreadLocal 中放基本类型,比如字符串、整型等,也可以放自定义的实体对象,还可以放 List、Set、Map 等都没有问题。

    图片图片

    先来理清楚 ThreadLocal 对象的结构与线程的关系,我解释一下上图的意思。

  • 在 Thread 类中有一个属性叫做 threadLocals,这个属性的类型是 ThreadLocal.ThreadLocalMap 类型;
  • ThreadLocal 就是我们会直接用到的 ThreadLocal 对象;
  • ThreadLocal 有个内部类 是 ThreadLocalMap,就是 Thread 类中的的 threadLocals 对象的类型;
  • ThreadLocalMap 通过名称可以看出这是一个 Map 结构,如果你看过 HashMap 的实现,就会发现它是个简易版的 HashMap;
  • ThreadLocalMap 中真正存储数据的是一个 Entry 数组;
  • Entry 又是ThreadLocalMap的一个静态内部类, 它继承 WeakReference 弱引用,暂且理解为是一个 key-value 键值对;其中涉及的重要对象大概就是上面这些,了解这些基础后,能帮我们更清楚的理解原理。
  • 看上去可能有点乱,最简单的就是从 set 方法入手看一看。下面是 set 方法代码

    public void set(T value) {
            Thread t = Thread.currentThread(); // 获取当前线程
            ThreadLocalMap map = getMap(t); // 获取当前线程维护的 threadLocals
            if (map != null)
                map.set(this, value); // 如果 map 不为空,直接添加
            else
                createMap(t, value); //如果 map 为空,先初始化,再添加
        }

    调用 set 方法

    ThreadLocal mLocal = new ThreadLocal();
    mLocal.set("hello");

    调用 ThreadLocal 的 set 方法时,首先获取到了当前线程。

    Thread t = Thread.currentThread();

    然后获取当前线程维护的 ThreadLocalMap 对象。通过 getMap() 方法,t 就是当前线程,直接返回当前线程中的 threadLocals 属性。

    ThreadLocalMap map = getMap(t); //获取 ThreadLocalMap
    
    ThreadLocalMap getMap(Thread t) {  
        return t.threadLocals;  
    }

    如果 map 不为null,说明之前设置过 ThreadLocal 了,那就调用ThreadLocalMap 的set 方法。

    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();  
    }
    int i = key.threadLocalHashCode & (len-1);

    计算索引,tab[i]就是要存储的位置,后面 for 中的部分就是处理哈希冲突和更新已有值,先不用管这些细节,之后将 new Entry(key, value)放到 tab[i]的位置,也就是放到 Entry 数组中了。

    这里面 new Entry中的参数 key 和 value 很关键。返回去看 ThreadLocalMap.set 方法调用时候的传参。

    map.set(this, value);

    key 是什么呢?key 这里传的是 this,this 是谁呢,就是 ThreadLocal 本身,它本身被当做 key 了。value 是什么呢?value 就是调用 ThreadLocal.set(value)时传过来的泛型的值,是我们调用方自己设置的。

    后面还有如果 ThreadLocalMap 实例不存在的话,则要初始化并赋初值的过程,这部分也不是理解 ThreadLocal 的重点,就不具体讲了,看代码都能理解。

    所以后面那三个问题也就解决了。

    现在再回过头去看最开始给的那段代码。

    threadLocal1 和 threadLocal2 的声明是在 main 方法中的,也就是在主线中声明的,三个子线程都可以看到的。

    而且线程0和线程1都用了两个 ThreadLocal,所以说,一个线程可以用多个 ThreadLocal,因为最终存储实际上是个 Map,多少个都没关系。

    线程 0 和线程1都对threadLocal1 和 threadLocal2重新设置值了,然后通过get方法得到的也是本线程设置的值。线程2没有对 threadLocal1 赋值,所以在调用get方法后,得到的是threadLocal1最开始设置的初始值,并不是线程0或线程2设置的值。也印证了线程之间是不会互相影响的(当然,我们通过上面的分析已经了解这个原理了)。

    内存泄漏问题

    实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

    所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

    ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

    这回,理解了吗?

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论