ThreadLocal原理与安全分析

TheadLocal介绍及用法

ThreadLocal是线程的本地变量。当使用ThreadLocal维护变量时,它为每个线程提供独立的变量副本。

每一个线程可以独立地操作自己的变量,不受其他线程影响。

使用场景

  • 作为数据副本,当某些数据是以线程为作用域并且不同线程有不同数据副本,使用ThreadLocal。
  • 保存线程上下文信息,在任意需要的地方可以获取,避免显示传参。
  • 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失。

ThreadLocal与Synchronized

ThreadLocal是与线程绑定的一个变量,其与Synchronized都用于解决多线程并发问题。

但Synchronized设计用于线程间变量共享 ;而ThreadLocal用于线程间变量隔离。

Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问 。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象。

Thread实现原理与内部结构

ThreadLocal

ThreadLocal由两个部分组成,ThreadLocal与ThreadLocalMap,后者为前者的一个静态内部类。

ThreadLocal的核心方法有三个:set() , get() 和remove() 。

ThreadLocalMap

ThreadLocalMap(简称TLM),TLM是ThreadLocal的核心。本质上,TLM是一个定制化的HashMap。

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

它继承了弱引用(WeakReference) ,键是ThreadLocal本体,值就是我们存入的value。在默认情况下, 每个线程中的这两个变量都为null。

在创建Map时,传入的Thread的内部成员变量会引用该TLM。

java 复制代码
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

方法解析

set()

java 复制代码
public void set(T value) {
        // 1.获取调用线程(当前线程)
        Thread t = Thread.currentThread();
        // 2.寻找当前线程的TLM
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 3.1如果TLM存在,则直接set
            map.set(this, value);
        } else {
            // 3.2如果TLM不存在,就新建TLM
            createMap(t, value);
        }
    }

getMap()方法:

返回当前线程引用的TLM。

java 复制代码
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

get()

java 复制代码
public T get() {
        // 1.获取当前线程
        Thread t = Thread.currentThread();
        // 2.获取TLM
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            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();
    }

setInitialValue():

java 复制代码
private T setInitialValue() {
        // 1.创建默认值,实际上就是null
        T value = initialValue();
        // 2-3.获取线程和TLM
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 4.根据map不同情况执行不同操作
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        // JDK11新增了TerminatingThreadLocal类,主要用于解决内存泄露问题。
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

remove():

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

本质

由以上分析我们可以知道,本地变量实际存放在调用线程的threadLocals,ThreadLocal只是一个封装壳。

ThreadLocal的核心是TLM。

ThreadLocal内存泄露问题

名词解释:

内存溢出(Out of Memory)

内存溢出是指程序在申请内存时,没有足够的内存供其使用。

内存泄露(MemoryLeak)

内存泄露是指程序在申请内存后,无法或者未释放已经申请到的内存。

MemoryLeak积少成多,会导致Out of Memory。

强引用与弱引用

TLM的Entry显式继承了WeakRereference的ThreadLocal。

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
​
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

引用类型一般在java.lang.ref包下,常见的引用有:FinalReference, SoftReference以及WeakReference。

强引用(StrongReference)

StrongReference在上述包下并没有实际的对应类,但我们程序中几乎所有的引用使用的都是强引用。

例如

java 复制代码
StringBuilder sb = new StringBuilder(); 
// 这里栈上的sb对堆上的StringBuilder对象是强引用

强引用可以直接访问目标对象,并且指向的对象任何时候都不会被回收,即使不回收该对象会抛出OOM(OutOfMemory),JVM也不会回收。

弱引用(WeakReference)

弱引用中的对象具有很短的声明周期,因为在系统GC时,只要发现弱引用,不管堆空间是否足够,都会将对象进行回收。由于垃圾回收器是一个优先级很低的线程 ,因此不一定立即 发现那些只具有弱引用的对象。

问题原因

ThreadLocal是WeakReference,如果一个ThreadLocal没有外部强引用来引用它,就会被GC回收。

ThreadLocalMap中就会出现key为null的Entry,key为null,则无法访问这些Entry的value。

而在线程池技术下,线程经常服用,生命周期非常长,甚至与JVM共生。

这样一条引用链会一直保持:Thread Ref-> Thread -> ThreaLocalMap -> Entry -> value。

这会导致value无法被回收,引起内存泄露。

ThreadLocal为什么要弱引用

如果使用强引用的话

ThreadLocalMap的生命周期基本和Thread的生命周期一样,当前线程如果没有终止,那么ThreadLocalMap始终不会被GC。ThreadLocalMap持有对ThreadLocal的强引用,那么ThreadLocal也不会被GC,当线程生命周期长,如果没有手动删除,则会造成Entry的累积,从而导致OOM。

从另一个方面来说,强引用只是在GC层面将内存泄露的问题掩盖起来,并没有真正解决问题。

由上可知:引起内存泄露的根本原因不是弱引用,而是因为TLM与Thread共生,即TLM生命周期和Thread保持一致。

所以,在使用时一定要使用完ThreadLocal后调用remove()方法清理。

相关推荐
天空鸟_时光不老5 分钟前
01-我不转Python把AI塞进Java里
java·人工智能·spring boot·后端·spring·spring cloud·架构
vx_Biye_Design17 分钟前
flask学生课程笔记共享系统29026-计算机课程设计、毕业设计
java·javascript·spring boot·后端·elasticsearch·flask·课程设计
代码山河18 分钟前
JDK、JRE、JVM的区别:一文讲清楚Java运行环境
java·学习·架构·教程·面向对象·项目
深入云栈31 分钟前
Netty 4.2.x 源码深度解析 (十七):Channel 底层读写 —— Unsafe 的 I/O 操作内核
java·后端·架构
SimonKing44 分钟前
Qoder中Qwen3.8-Flash 限时免费使用
java·后端·程序员
云和数据.ChenGuang1 小时前
langchain4j InMemoryEmbeddingStore常用的方法
java·人工智能·windows·java-ee·fastapi·springai
小番茄程序猿1 小时前
RAG 的 precision 只有 0.55,我以为检索烂了,MRR 一测才发现是我用错了尺子
java·后端
天空鸟_时光不老1 小时前
07-检查点与状态持久化
java·人工智能·spring boot·spring·spring cloud·kafka·maven
GoodStudyAndDayDayUp1 小时前
一个简单的java jar跑docker
java·docker·jar
Nebula_g1 小时前
JavaSE加强:Commons-io框架
java·开发语言·算法·javase