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

相关推荐
Jason-河山6 分钟前
利用 Python 爬虫采集 1688商品详情
java·http
计算机源码社6 分钟前
分享一个餐饮连锁店点餐系统 餐馆食材采购系统Java、python、php三个版本(源码、调试、LW、开题、PPT)
java·python·php·毕业设计项目·计算机课程设计·计算机毕业设计源码·计算机毕业设计选题
Zww089110 分钟前
idea插件市场安装没反应
java·ide·intellij-idea
夜雨翦春韭11 分钟前
【代码随想录Day31】贪心算法Part05
java·数据结构·算法·leetcode·贪心算法
计算机学姐11 分钟前
基于微信小程序的调查问卷管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
problc22 分钟前
Android 组件化利器:WMRouter 与 DRouter 的选择与实践
android·java
程序员南飞2 小时前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
弥琉撒到我2 小时前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
一颗花生米。3 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼3 小时前
Java基础-单例模式的实现
java·开发语言·单例模式