ThreadLocal:熟悉而又陌生

Hi,大家好,我是抢老婆酸奶的小肥仔。

在很多的地方,我们都能看到ThreadLocal的身影,也会用到它,但是我们真的就了解它吗?

今天我们来叨叨这个我们既熟悉又陌生的小伙伴,废话不多说开整。

1、啥是ThreadLocal

一言以蔽之:线程各行其是,即线程间的隔离性。

在多线程时,访问同一个共享变量,可能会存在线程安全问题,为了线程安全,我们会为这个变量加锁,以达到同一时间只能有一个线程进行访问,其他线程只能等待。这样就会导致程序复杂性,开发人员也必须对锁的使用特别熟练,否则可能产生死锁。

ThreadLocal可以将创建的变量作为当前线程私有变量

我们通过代码来看看ThreadLocal是否是只操作线程本身的私有变量的。

java 复制代码
/**
 * @author: jiangjs
 * @description: 使用ThreadLocal
 * @date: 2023/7/28 14:55
 **/
public class UseThreadLocal {
    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            tl.set("线程A名称【" + Thread.currentThread().getName() + "】");
            System.out.println("获取当前线程A的名称:" + tl.get());
            tl.remove();
            System.out.println("验证是否删除当前线程A的名称:" + tl.get());
        }).start();

        new Thread(() -> {
            tl.set("线程B名称【" + Thread.currentThread().getName()+"】");
            System.out.println("获取当前线程B的名称:" + tl.get());
            System.out.println("验证是否删除当前线程B的名称:" + tl.get());
        }).start();
    }
}

执行结果:

上述代码中,我们通过创建两个线程,分别在定义的ThreadLocal中添加了各自的线程名称,但是在线程A中调用了ThreadLocal提供的remove方法进行了删除。我们发现线程A的线程名称被删除了,而线程B并未受影响,因此,ThreadLocal做到了隔离性,线程间的变量互不干扰。

2、ThreadLoal原理

我们翻开ThreadLocal的源码,会发现其内部有一个ThreadLocalMap的内部静态类。根据这个静态内部类上的注释我们可以了解,这是一个定制的散列映射,只适合于维护线程本地值。

在ThreadLocal简介里面我们用到了三个方法:set(T value) ,get(),remove()三个方法,我们来看看他们的源码,通过他们的源码来了解ThreadLocal的原理。

2.1 set(T value)

set(T value)方法源码:

java 复制代码
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //根据当前线程,获取ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //ThreadLocalMap对象不为空,则直接赋值
        map.set(this, value);
    else
        //ThreadLocalMap对象为空,则创建对象,赋值
        createMap(t, value);
}

上述源码中我们知道,ThreadLocalMap则是以当前线程作为key,而传递的value则是ThreadLocalMap保存的值。

在上述源码中,我们用到两个内部方法:getMap(t), createMap(t, value),我们也顺便看看这两个方法的源码。

getMap(t)源码:

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

源码返回的是当前线程中的threadLocals变量:ThreadLocal.ThreadLocalMap threadLocals = null; 因此,当我们第一次调用时,返回的就是null。

createMap(t, value)源码:

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

在createMap()中,则是直接调用ThreadLocalMap的构造方法创建对象,并赋值给线程的threadLocals变量。

上述的源码比较好理解,也就是获取当前线程,通过当前线程获取自身的成员变量threadLocals,而threadLocals其实就是TheadLocal的ThreadLocalMap对象,如果对象不为null,则直接调用set方法赋值,否则创建ThreadLocalMap对象后并实例化当前线程的threadLocals成员变量。

2.2 get()

get()方法源码:

java 复制代码
public T get() {
    Thread t = Thread.currentThread();
    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;
        }
    }
    return setInitialValue();
}

上述源码,获取当前线程的ThreadLocalMap对象即threadLocals成员变量,如果为空,则调用setInitialValue()方法初始化threadLocals成员变量的值,否则直接返回绑定的本地变量。

java 复制代码
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;
}

initialValue() :返回的是一个null值。因此如果get()获取不到值时,则直接返回的就是null,setInitialValue()的方法也是调用createMap(t, value)创建ThreadLocalMap,只不过传递的值为null。

2.3 remove()

remove()方法源码:

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

上述源码就比较简单,获取当前线程的ThreadLocalMap对象,如果不为空,则删除value值。

2.4 关于ThreadLocalMap

在上述的ThreadLocal的三个方法中,其本质都是操作ThreadLocalMap,我们通过createMap中初始化ThreadLocalMap时调用的构造方法来看看其本质:

java 复制代码
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);
}

INITIAL_CAPACITY:定义Entry数组的长度,值:16。

threadLocalHashCode:获取当前线程的hash值。

setThreshold(INITIAL_CAPACITY) :计算扩容因子。

Entry源码:

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

从源码中,我们可以看出Entry继承了WeakReference,ThreadLocal作为key是一个弱引用,而弱引用在JVM的每一次GC时都会被回收。

3、ThreadLocal一些特性

3.1 变量不具有传递性

简单来说:同一个ThreadLocal在父线程中设置值后,子线程也是无法获取的。

其实不难理解,毕竟ThreadLocal主打的就是一个变量的隔离性。

我们也用代码来验证一下。

java 复制代码
public class UseThreadLocal {
    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        tl.set("获取的值");
        new Thread(() -> {
            System.out.println("获取主线线程的值:" + tl.get());
        }).start();
    }
}

执行结果:

如果想要子线程获取父线程的值则可以使用:InheritableThreadLocal。

3.2 关于OOM

3.2.1 原因

在ThreadLocalMap中,其定义的Entity是继承了WeakReference,并指定ThreadLocal<?> k做为key,且是一个弱引用,当ThreadLocal作为key在没有外部强引用时,就会被GC回收,而value作为强引用不会被回收,就会造成存在key为null,value不为null的Entity,此时若线程一直不结束,或线程作为线程池中的一员,即使结束也不会被销毁,这样久而久之就可能造成OOM。

3.2.2 如何避免OOM呢?

1、使用完ThreadLocal后,调用remove()方法清除数据

2、将ThreadLocal变量使用private static,使其一直存在强引用,同时能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

4、总结

ThreadLocal采用了多线程隔离机制,在多线程下线程将共享变量复制一个副本,线程各自只能使用线程本身的副本变量,为提供了访问变量的安全性。同时采用了空间换时间的思想,通过ThreadLocalMap来管理线程成员变量信息。

好了,今天就跟大家叨叨到这,谢谢大家。

相关推荐
落落落sss6 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己6 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_8532757326 分钟前
ArrayList 源码分析
java·开发语言
爪哇学长31 分钟前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
大鲤余36 分钟前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万44 分钟前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
深情废杨杨1 小时前
后端-实现excel的导出功能(超详细讲解)
java·spring boot·excel