Java的ThreadLocal详解

1. 什么是ThreadLocal

先来看看官方的定义。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

此类提供线程局部变量,这些变量与普通变量不同,每个访问他们的线程都有自己独立初始化的变量副本。ThreadLocal实例通常是用private static修饰的字段,希望将状态和线程关联(比如用户id,事物id)。

这里很明确的一点:每个线程独立拥有自己的线程局部变量,因此是线程安全的。那么线程局部变量是什么?

应用场景:

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离。
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。

2 简单代码示例

public class ThreadLocalTest {

    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal1.set("My name is threadLocal1");
            System.out.println("thread1 name:" + threadLocal1.get());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("thread2 name:" + threadLocal1.get());
        });
        thread1.start();
        thread2.start();
    }
}

结果:

thread2 name:null
thread1 name:My name is threadLocal1

为什么thread2线程threadLocal1.get()没有获得My name is threadLocal1呢?

3 实现原理

我们通过源码分析其实现原理。

3.1 首先看ThreadLocal.set()方法

在这里插入图片描述

 public void set(T value) {
 		// (1)获得当前线程,即调用线程
        Thread t = Thread.currentThread();
        // (2)以当前线程为参数,获得当前线程的map,这个就是线程局部变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	// (3)如果map不为空,则以ThreadLocal实例引用为key保存vaue值
            map.set(this, value);
        else
        	// (4)如果map为空,则创建map,并保存value。
            createMap(t, value);
    }
  • (2)处的getMap方法:
    在这里插入图片描述
    再点击t.threadLocals得到:
    在这里插入图片描述
    可以看到线程持有一个ThreadLocal.ThreadLocalMap类型的类变量threadLocals,这个变量就是线程局部变量,这就是为什么线程之间互不影响的原因。

  • (3)处map.put()方法

private void set(ThreadLocal<?> key, Object value) {
     Entry[] tab = table;
     int len = tab.length;
  	 ...
 }

这里的key就是示例代码中的变量threadLocal1的 引用。很多文章说是当前线程,其实是不对的。

  • (4)处createMap()方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

如果线程的threadLocals为null,则新建一个,并且设置value值。这里的this是变量threadLocal1的 引用

3.2 再看ThreadLocal.get()方法

在这里插入图片描述

public T get() {
		// (1)获得当前线程,即调用线程
        Thread t = Thread.currentThread();
        // (2)以当前线程为参数,获得当前线程的map,这个就是线程局部变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// (3)获取value值,这里的this还是变量**threadLocal1**的引用
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // (4) 初始化线程的threadLocals变量
        return setInitialValue();
    }

3.3 Thread、ThreadLocalMap、ThreadLocal总览图

在这里插入图片描述
线程1和线程2各自持有类型为ThreadLocal.ThreadLocalMap的threadLocals变量,key是ThreadLocal的引用,value为线程各自需要保持的值。

一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal,这也是threadLocals是一个类似java.util.Map的原因。
在这里插入图片描述

3.4 总结

  • 每个Thread维护着一个ThreadLocalMap的引用
  • ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  • ThreadLocalMap的键key为ThreadLocal对象,一个ThreadLocal只能保持一个Object对象,如果要保存多个,就需要创建多个ThreadLocal对象。

4 ThreadLocal内存泄漏问题

4.1 几个概念

  • 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

  • 软引用:如果一个对象具有软引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。

  • 弱引用:如果一个对象只具有弱引用,那么这个对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。

  • 虚引用:所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。对象随时可能被GC掉。

  • 内存泄漏:对象占用着内存,但是因为忘记在哪里,不能被回收,当这种对象越来越多时,内存不过,服务宕机。

4.2 源代码

static class ThreadLocalMap {

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

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

ThreadLocal对象是弱引用而作为Entry的key。当ThreadLocal对象没有强引用,当发生GC时,势必会被GC掉,此时key为null,但是value的强引用还在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,如果线程不结束,value就不会被GC,造成内存泄漏。线程池使用ThreadLocal很可能造成内存泄漏,特别注意。
在这里插入图片描述

4.3 建议

  • JDK建议用private static修饰ThreadLocal;
  • ThreadLocal使用完后,及时remove。

参考文章:

https://www.cnblogs.com/fsmly/p/11020641.html#_label4
https://mp.weixin.qq.com/s/rNcarCQI2oVjyJdVR7TaAw
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
https://www.cnblogs.com/xzwblog/p/7227509.html
http://www.threadlocal.cn/

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值