简洁而不简单—ThreadLocal

简单概述

ThreadLocal叫做线程变量 ,一句话描述,他就是属于当前线程的独有变量。

我们使用ThreadLocal的时候,设置的是同样的Key,但是他的Value在每个线程中是不同的。每一个使用这个变量的线程都会初始化一个完全独立的实例副本。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

数据结构

可以看到每个Thread都有一个ThreadLocalMap ,既然叫map,就有key和value,这里面的key就是一个ThreadLocal的弱引用 ,value就是放入的Object值。每个线程往ThreadLocal里面放值的时候都会存到ThreadLocalMap里面,读也是以TheadLocal作为引用去map里面读。

总结,一个线程里面有个map,可以放很多threadlocal,threadlocal本身不是map结构,他只是这个map中的一个key。

简单实用

csharp 复制代码
public class ThreadLocalUtils {
  private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();
​
  public static void set(String s) {
    threadLocal.set(s);
  }
​
  public static void get() {
    System.out.println(threadLocal.get());
  }
​
  public static void remove() {
    threadLocal.remove();
  }
}
​
public class Main {
  public static void main(String[] args) {
    ThreadLocalUtils.set("hello");
    ThreadLocalUtils.get();
    ThreadLocalUtils.remove();
    ThreadLocalUtils.get();
  }
}
​

输出

源码分析

scss 复制代码
 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

获得当前线程,如果当前ThreadLocalMap不为空,那就赋值,否则初始ThreadLocalMap并且赋值。

scala 复制代码
  static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

可以看到ThreadLocalMap内部有个Entry来保存数据,这个Entry继承弱引用。在Entry内部使用ThreadLocal作为key

ini 复制代码
//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

初始化方法

scss 复制代码
    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return 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;
    }

get方法也比较简单,就是两种情况,一种get的key map中存在就直接返回得到的result,如果key不存在那就初始化这个threadLocalMap,并且以传入的threadLocal作为key,值为null作为第一个Entry。

csharp 复制代码
 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

删除方法就是在当前线程中取出map,map删除掉这个Entry。

引出内存泄漏问题

实际上ThreadLocal中的Key是ThreadLocal的弱引用,也就是在GC的时候会被清理掉,这样ThreadLocalMap使用这个ThreadLocal的key也会被清理掉,但是value是强引用,不会被清理。这样可能会出现key为null的value

弱引用:发生GC的时候就会回收掉弱引用对象

强引用:不会被GC回收

这个value和线程的生命周期同步,但是在线程池环境下,线程生命周期可能和JVM同步,因此会发生内存泄漏,如果不断取线程写入大对象,不及时remove,久而久之所泄漏的内存空间就会持续膨胀。下面引出四个问题

  1. 为什么key不能是强引用? 强引用的key会导致内存泄漏问题,因为ThreadLocalMap中的key通常是ThreadLocal实例,如果使用强引用,即使线程不再需要该ThreadLocal实例,由于强引用的存在,该实例也无法被垃圾回收,从而造成内存泄漏。
  2. 为什么key选择使用弱引用? 使用弱引用的key可以避免内存泄漏问题。当线程不再需要该ThreadLocal实例时,如果该实例只被ThreadLocalMap中的弱引用key引用,而没有其他强引用指向它,垃圾回收器就可以自动回收这部分内存,释放对应的ThreadLocal实例。
  3. 为什么value使用强引用? value使用强引用是为了确保在需要时能够正常访问和使用对应的值。如果value使用弱引用,当线程需要访问该值时,如果此时该值被垃圾回收器回收了,就无法正常获取到预期的值,破坏了ThreadLocal的预期行为。
  4. 为什么value不使用弱引用? 如果value使用弱引用,可能会导致在线程仍需要该值时,值被过早回收的情况发生,这不符合ThreadLocal的设计初衷,即确保每个线程都能获取到自己设置的值,而不受垃圾回收的影响。因此,value通常使用强引用,以保证线程可以正常地访问和操作它们。
相关推荐
qq_4419960515 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼22 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring