简洁而不简单—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通常使用强引用,以保证线程可以正常地访问和操作它们。
相关推荐
suweijie7682 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿3 小时前
List深拷贝后,数据还是被串改
java
向前看-6 小时前
验证码机制
前端·后端
xlsw_6 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹7 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭7 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫8 小时前
泛型(2)
java
超爱吃士力架8 小时前
邀请逻辑
java·linux·后端
南宫生8 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石8 小时前
12/21java基础
java