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()方法清理。

相关推荐
iam_leeqing5 分钟前
Lambda表达式
java·spring
你我约定有三9 分钟前
设计模式--适配器模式
java·设计模式·适配器模式
JouJz35 分钟前
设计模式之代理模式:掌控对象访问的优雅之道
java·spring·设计模式·系统安全·代理模式
lovix1236 分钟前
java进阶(三):单例、工厂、模版方法与代理模式详解
java·开发语言
ta叫我小白1 小时前
Spring Boot 设置滚动日志logback
java·spring boot·spring·logback
永卿0011 小时前
设计模式-观察者模式
java·前端·设计模式
C雨后彩虹1 小时前
行为模式-观察者模式
java·观察者模式·设计模式
天天摸鱼的java工程师1 小时前
百万数据导出Excel:从新手坑到老鸟方案
java·后端·面试
愿你天黑有灯下雨有伞1 小时前
从数据库到播放器:Java视频续播功能完整实现解析
java·数据库·音视频
hqxstudying1 小时前
Java行为型模式---观察者模式
java·开发语言·windows·观察者模式