JUC进阶06——ThreadLocal

ThreadLocal

概述

ThreadLocal 是 Java 中的一个类,它提供了一种线程局部(thread-local)变量。这种变量与普通的变量不同,每一个访问这个变量的线程都有它自己的独立初始化的变量副本。

简单来说,它能够提供线程内的局部变量,这些变量对其他线程而言是隔离的。这意味着每一个线程访问自己内部的ThreadLocal变量时,都是在操作自己独立的、线程封闭的数据副本。ThreadLocal非常适合于实现线程安全的场景,无需借助额外的同步措施
ThreadLocal 实例通常作为静态私有字段存在于类中,它们被多线程访问的类使用,以保存线程的状态信息

ThreadLocal 使用

API 介绍

java 复制代码
/**
 * 返回此线程局部变量对于当前线程的"初始值"。当线程首次通过 {@link #get} 方法访问变量时,
 * 会调用此方法,除非线程之前已经通过 {@link #set} 方法设置了值,在这种情况下,对于该线
 * 程将不会调用 {@code initialValue} 方法。通常,此方法在每个线程中最多被调用一次,但
 * 如果随后调用了 {@link #remove} 方法并再次调用 {@link #get} 方法,则可能会再次调用此方法。
 *
 * <p>此实现简单地返回 {@code null};如果程序员希望线程局部变量具有除 {@code null} 
 * 以外的初始值,则必须子类化 {@code ThreadLocal},并重写此方法。通常,会使用匿名内部类来实现。
 *
 * @return 此线程局部变量的初始值
 */
protected T initialValue();

/**
 * 创建一个线程局部变量。变量的初始值由对给定 {@code Supplier} 调用 {@code get} 方法来确定。
 *
 * @param <S> 线程局部变量的值的类型
 * @param supplier 用于确定初始值的提供者
 * @return 一个新的线程局部变量
 * @throws NullPointerException 如果指定的提供者为 null
 * @since 1.8
 */
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier);

/**
 * 返回当前线程对此线程局部变量副本中的值。如果当前线程中该变量没有值,它将
 * 被初始化为调用 {@link #initialValue} 方法返回的值。
 *
 * @return 此线程局部变量在当前线程中的值
 */
public T get();

/**
 * 将此线程局部变量当前线程的副本设置为指定值。大多数子类无需重写此方法,
 * 仅依赖 {@link #initialValue} 方法来设置线程局部变量的值。
 *
 * @param value 要存储在此线程局部变量当前线程副本中的值
 */
public void set(T value);

/**
 * 移除此线程局部变量当前线程的值。如果当前线程随后 {@linkplain #get 读取} 
 * 此线程局部变量,其值将通过调用其 {@link #initialValue} 方法重新初始化,
 * 除非在此期间其值被当前线程通过 {@linkplain #set 设置}。这可能导致在
 * 当前线程中多次调用 {@code initialValue} 方法。
 *
 * @since 1.5
 */
public void remove();

ThreadLocal 使用案例

  • 统计每个线程kick次数
java 复制代码
package com.avgrado.demo.thread;

import lombok.var;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName ThreadLocalDemo
 * @Description ThreadLocalDemo
 * @Date 2024-03-11 15:19
 */
class KickBall{
     int count;
    //用来统计总数
    public synchronized void  kickCount(){
        count++;
    }
    
    //定义 ThreadLocal (此处要记得初始化否则会报空指针)
    static ThreadLocal<Integer> kickCountValue = ThreadLocal.withInitial(() -> 0);
    
    public void kickCountByThreadLocal(){
        //这里设置值要先取之前的值,所以需要初始化
        kickCountValue.set(kickCountValue.get()+1);
    }
}
public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        KickBall kickBall = new KickBall();
        for(int i=0;i<5;i++){
            new Thread(()->{
                var size = new Random().nextInt(10) + 1;

                for(int j=0;j<size;j++){
                    kickBall.kickCount();
                    kickBall.kickCountByThreadLocal();
                }
                System.out.println("线程:"+Thread.currentThread().getName()+" 踢了"+KickBall.kickCountValue.get()+"次");
            },String.valueOf(i+1)).start();

        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("总共踢了"+kickBall.count+"次");
    }
}
--------------------------------------
线程:3 踢了9次
线程:4 踢了1次
线程:1 踢了5次
线程:2 踢了9次
线程:5 踢了9次
总共踢了33次
  • web应用中用来存储用户信息
java 复制代码
public class UserContext {
    private static final ThreadLocal<User> threadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        threadLocal.set(user);
    }

    public static User getUser() {
        return threadLocal.get();
    }

    public static void removeUser() {
        threadLocal.remove();
    }
}

// 在请求处理过程中使用  
public class RequestHandler {
    public void handleRequest() {
        User user = // ... 获取当前请求的用户信息  
                UserContext.setUser(user);

        // ... 执行请求处理逻辑,可以使用 UserContext.getUser() 获取用户信息  

        UserContext.removeUser(); // 请求处理完成后移除用户信息  
    }
}
  • 日志中记录线程id
java 复制代码
public class ThreadContext {
    private static final ThreadLocal<Long> threadId = ThreadLocal.withInitial(() -> Thread.currentThread().getId());

    public static Long getThreadId() {
        return threadId.get();
    }
}

// 在日志记录中使用  
logger.info("Thread ID: {} - Some log message", ThreadContext.getThreadId());

ThreadLocal 源码解读分析

  • 先看Thread、ThreadLocal、ThreadLocalMap之间的关系

从上面的图中可以看到:
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,在每个线程初始化时都会创建一个属于自己的ThreadLocalMap,用于存储该线程的ThreadLocal变量。 ThreadLocalMap是ThreadLocal中的一个静态内部类,ThreadLocal 通过ThreadLocalMap来实现局部变量的存储。 ThreadLocalMap中定义了一个Entry的静态内部类,其中 Entry 构造方法中的 ThreadLocal<?> k 参数是ThreadLocal 实例Object v 是存储的ThreadLocal变量的value值

ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象

当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放

  • ThreadLocalMap 构造方法及相关参数源码
java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

/**
 * The initial capacity -- MUST be a power of two.
 * 初始容量,必须为2的倍数.
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * The table, resized as necessary.
 * 必要时会扩容
 * table.length MUST always be a power of two.
 * 数组的大小必须是2的倍数
 */
private Entry[] table;

/**
 * 数组扩容阈值,初始值为0,创建了ThreadLocalMap对象之后会被重新设置
 */
private int threshold; // Default to 0

/**
 *ThreadLocalMap的构造器,ThreadLocal作为key
 * Construct a new map initially containing (firstKey, firstValue)
  初始化一个新的Map,初始化时包含(firstMKey,firstValue)
  
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it
 ThreadLocalMap是懒加载构造的,因此只有在我们创建了一个Entry对象并且放到 Entry数组的时候才会初始化Entry数组
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化一个Entry 容量大小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);
}
  • ThreadLocal中set方法
java 复制代码
/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap实例,返回的是 threadLocals
    ThreadLocalMap map = getMap(t);//参考下面的getMap方法
    //map不为空,就把参数放到map中去
    if (map != null)
        map.set(this, value);
    //为空,就新建map
    else
        createMap(t, value);
}

    /**
     * 获取当前线程的ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    
    /**
     * 创建一个ThreadLocalMap.
     */
    void createMap(Thread t, T firstValue) {
        //这里的this是指调本地方法的ThreadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocal类中的set方法用于将当前线程的ThreadLocalMap中的某个ThreadLocal对象与指定的数值关联起来。以下是set方法的详细执行过程:

  1. 首先,set方法会获取当前线程的ThreadLocalMap对象。如果当前线程没有ThreadLocalMap对象,则会先创建一个并关联到当前线程。
  2. 接下来,set方法会以当前ThreadLocal对象作为key,要存储的值作为value,插入到ThreadLocalMap中。
  3. 如果当前线程没有ThreadLocalMap对象,会先创建一个并赋给当前线程。
  4. 在将ThreadLocal对象与值关联起来之前,需要判断是否有可能存在哈希冲突,如果有冲突则需要解决冲突。
  5. 如果ThreadLocalMap内部的Entry数组已满,会进行扩容操作来增加容量,保证能够存放新的Entry
  • ThreadLocal中的get方法
java 复制代码
 
    public T get() {
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程对象中维护的ThreadLocal对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //以当前的ThreadLocal为key。调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取存储实体e对应的value值,即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
            初始化:两种情况执行当前代码
                1.map不存在,表示此线程没有维护的ThreadLocalMap对象
                2.map存在,但是没有与当前ThreadLocal关联的key
        */
        return setInitialValue();
    }
 
    //返回初始化后的值
    private T setInitialValue() {
        //调用initialValue获取初始化的值
        //此方法可以被子类重写,如果补充些默认返回null
        T value = initialValue();
        //获取当前线程对象
        Thread t = Thread.currentThread();
        //获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        //1、当前线程Thread不存在ThreadLocalMap对象
        //2、则调用createMap进行ThreadLocalMap对象初始化
        //3、并将t(当前线程)和value(t对应的值)作为第一个entry存放到ThreadLocalMap中
            createMap(t, value);
        return value;
    }

ThreadLocal类中的get方法用于获取当前线程的ThreadLocalMap中与指定ThreadLocal对象关联的值。以下是get方法的详细执行过程:

  1. 首先,get方法会获取当前线程的ThreadLocalMap对象。
  2. 然后,get方法会以当前ThreadLocal对象作为key,在ThreadLocalMap中查找对应的值。
  3. 如果找到了与ThreadLocal对象关联的值,则将该值返回;如果没有找到,则返回null。
  4. 在查找过程中,需要注意处理哈希冲突的情况,ThreadLocalMap内部使用线性探测法来解决哈希冲突。
  5. 另外,由于ThreadLocalMap中的Entry是使用弱引用来引用ThreadLocal对象的,当ThreadLocal对象没有被外部强引用持有时,可能会被垃圾回收器回收,这也是为什么需要注意内存泄漏问题的原因之一。
  • ThreadLocal中 remove 方法
csharp 复制代码
/**
 * 清除当前线程保存的ThreadLocal对象,并删除该ThreadLocal对应的entry实体
 */
 public void remove() {
     //获取当前线程的ThreadLocalMap
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         //如果map不为空,就删除
         m.remove(this);
 }

ThreadLocal类中的remove方法用于移除当前线程的ThreadLocalMap中与指定ThreadLocal对象关联的键值对。以下是remove方法的详细执行过程:

  1. 首先,remove方法会获取当前线程的ThreadLocalMap对象。
  2. 然后,使用指定的ThreadLocal对象作为key,在ThreadLocalMap中查找对应的Entry。
  3. 如果找到了匹配的Entry,则将该Entry从ThreadLocalMap中移除。
  4. 在移除Entry时,需要注意解决哈希冲突的情况,ThreadLocalMap内部使用线性探测法来处理哈希冲突。
  5. 如果移除操作之后,ThreadLocalMap中没有其他的Entry存在,那么为了节省内存,可能会将整个ThreadLocalMap对象从当前线程中移除或清空

ThreadLocal 内存泄漏问题

什么是内存泄露

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏

ThreadLocal引用示意图:

从图中可以看出,ThreadLocal对象有两个引用,一个是栈内存中 ThreadLocal引用,另一个是ThreadLocalMap中的key引用。 value引用只有thread对象

当栈内存中ThreadLocal引用不存在了,堆内存中ThreadLocal对象还有entry中key这条引用链存在,会导致该对象无法回收。

为了解决这个问题,ThreadLocal使用了弱引用

在系统GC时,会回收掉这个ThreadLocal对象。但是这样就会导致ThreadLocalMap中就会出现keynullEntry,也就无法访问这些keynullEntryvalue,因为value的引用链时一条强引用生命周期是和thread一样的,只要thread一直存在(在使用线程池的情况下),这些keynullEntryvalue就会一直存在一条强引用链:
Thread引用 --> Thread对象 --> ThreaLocalMap --> Entry --> value引用 -->value

会导致对象永远无法回收,造成OOM

强、软、弱、虚 引用

  • 强引用: 对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一。

  • 软引用: 是一种相对强引用弱化了一些的引用,对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,他会被回收,软引用通常用在对内存敏感的程序中,比如高速缓存,内存够用就保留,不够用就回收。

  • 弱引用:比软引用的生命周期更短,对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

    • 软引用和弱引用的使用场景----->假如有一个应用需要读取大量的本地图片:
      • 如果每次读取图片都从硬盘读取则会严重影响性能
      • 如果一次性全部加载到内存中又可能会造成内存溢出,此时使用软应用来解决,设计思路:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题
  • 虚引用:虚引用必须和引用队列联合使用,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。

    虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制。换句话说就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作

如何解决内存泄漏

ThreadLocal的get、set、remove等方法被调用的时候,内部会实际调用ThreadLocalMap的get、set、remove等操作。

而ThreadLocalMap的每次get、set、remove,都会清理key为null,但是value还存在的Entry。

因此,我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug

总结

  • ThreadLocal并不解决线程间共享数据的问题
  • ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有他的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法
相关推荐
吃面不喝汤661 小时前
Flask + Swagger 完整指南:从安装到配置和注释
后端·python·flask
讓丄帝愛伱2 小时前
spring boot启动报错:so that it conforms to the canonical names requirements
java·spring boot·后端
weixin_586062022 小时前
Spring Boot 入门指南
java·spring boot·后端
凡人的AI工具箱8 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀8 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
canonical_entropy9 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构
我叫啥都行9 小时前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
无名指的等待71210 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴11 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
AskHarries11 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端