图解,深入浅出带你理解ThreadLocal

本文详细介绍了Java中的ThreadLocal类,包括其作用、如何使用、内部实现机制,特别是弱引用来防止内存泄漏的策略,以及inheritableThreadLocals的特性,强调了在多线程环境下正确管理和使用ThreadLocal的重要性。
摘要由CSDN通过智能技术生成


前言

ThreadLocal 是我们在学习java时必须优先掌握的内容,而且应用场景广泛。比如以前一些项目,会把前台传的一部分参数放入ThreadLocal,随线程流转;又或者我们经常使用的Spring框架的@Transactional注解,也用到了ThreadLocal。

所以学习ThreadLocal,是一个必备,且越早学习越好的基础内容


一、ThreadLocal是什么?

ThreadLocal 是一个类,和线程有关,但并不是一个Thread。

这个类能够提供线程局部变量

也就是说这个类的很多属性都被包含在线程实例中,比如下图就是Thread的字段展示,我们可以看到有一个threadLocals,它的实际类型是ThreadLocal.ThreadLocalMap

在这里插入图片描述
当然,他和普通的变量有所不同。它本身是唯一的对象,你可以把它new出来,但每个线程去访问或者设置它的时候,读取和设置的并不是这个对象,而是本线程内,这个对象的副本。

这也意味着,这个对象在不同的线程中,副本的值是不一样的

二、如何使用

代码如下:

public class ThreadlocalTest {
    static ThreadLocal<String> mylocalVar = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                mylocalVar.set("ABCDE");
                //打印本地变量
                System.out.println("thread1 val: " + mylocalVar.get());
            }
        });
        t1.start();
        Thread.sleep(1000);
        System.out.println("main thread val: " + mylocalVar.get());
    }
}

我们先new了这么一个ThreadLocal 出来,然后使用一个线程去set一个值,然后分别在该线程和主线程内读取这个ThreadLocal,看看结果

在这里插入图片描述
答案不出所料,t1 线程设置的值,只有 t1 自己能看到。主线程是看不到的。

这里的要点有二个:

  • ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
  • ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法

三、ThreadLocal 实现及图解

1. 源码解析

public class ThreadLocal<T> {
    public T get() {
    	Thread t = Thread.currentThread();  // 任何一个线程查询值时,都会获取当前线程
           // 返回当前线程里的map属性,线程类Thread中,有一个名为threadLocals,
           // 类型为ThreadLocalMap的成员变量,返回该成员变量
    	ThreadLocalMap map = getMap(t);  
    	if (map != null) {
                   // 这个成员变量实际就是个HsahMap,把本TreadLocal对象传进去,
                   // 即上文的mylocalVar对象,可返回该线程下+该treadlocal对象的存储值
        		ThreadLocalMap.Entry e = map.getEntry(this);
        		if (e != null) {
                    @SuppressWarnings("unchecked")
            	T result = (T)e.value;
            	return result;
        		}
    	}
    return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    public void set(T value) {
    	Thread t = Thread.currentThread();  
    	ThreadLocalMap map = getMap(t);  
    	if (map != null)
        	   map.set(this, value);
    	else
       	   createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
    	return t.threadLocals;
    }
}

public class Thread implements Runnable {
    //...
    // 此处类型为ThreadLocal类下的静态内部类ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;   
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 
}

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;  //重要,最终的数据是存储在一个Entry数组中
        static class Entry extends WeakReference<ThreadLocal<?>> {
        	Object value;
        	Entry(ThreadLocal<?> k, Object v) {
            	super(k);  //仅有key值使用了super(),即为key创建弱引用
            	value = v;
        	}
        }
        //...
        private Entry getEntry(ThreadLocal<?> key) {
        	int i = key.threadLocalHashCode & (table.length - 1);
        	Entry e = table[i];
        	if (e != null && e.get() == key)
            	return e;
        	else
            	return getEntryAfterMiss(key, i, e);
        }
        private void set(ThreadLocal<?> key, Object value) {
        }
        private void remove(ThreadLocal<?> key) {
    	Entry[] tab = table;
    	int len = tab.length;
    	int i = key.threadLocalHashCode & (len-1);
    	for (Entry e = tab[i];
         	  e != null;
             e = tab[i = nextIndex(i, len)]) {
        	  if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
            }
          }
        }
    }
}

2. 结构图解

我们先借用一张很好的图:每个线程里有map,map的结构我们都懂,就是一个个Entry构成的数组。而每一个Entry我们也是知道的,包含key 和 value。所谓的key,就是我们 new 出来的threadlocal对象。
在这里插入图片描述
结构解析

该功能的实现其实由两部分组成,1. 一部分是每一个线程对象里都持有一个map对象,所以不同线程里有不同线程对象,不同对象互不干扰;2. 另一个部分是每定义一个threadlocal对象,这个对象都会作为key用来帮助每个线程在map里定位

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  • 我们new出来的threadlocal对象,仅仅作为一个key
  • 每个Thread线程内部都有一个Map,Map里面存储threadlocal对象(key)和本线程的对该threadlocal对象设定的值(value)

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

3. 弱引用

ThreadLocalMap的内部类 Entry 被设计为实现了WeakReference,Entry用来存放数据。在构造Entry对象时,将传进来的ThreadLocal (key)对象包装成了弱引用对象,而Entry对象和内部的value对象之间是强引用的

在这里插入图片描述


四、内存泄漏

我们上面说了 ThreadLocal 里使用了弱引用,为什么这里要弄出这个特殊设计呢? 我们继续分析

因为目前程序运行大多采用的是线程池模式,线程存在时间很长,如果不断往其中加入线程私有对象而得不到回收,会导致OOM。所以为了减少程序员手动回收,同时兼顾避免OOM,设计了一套弱引用自动回收机制

当使用 mylocalVal = null 的时候,断绝 ClassA 对象和 mylocalVal 对象的关系,这样由线程持有的mylocalVal 就只有一个来自Entry的虚引用(虚线部分)了,我们知道,仅有虚引用的对象会被自动回收:
在这里插入图片描述

但是,需要注意的是,尽管采用了Entry—弱引用—key,来保证当 mylocalVal 对象置空时,回收Entry中的key,但此时 Entry 以及 value仍然存在,依然存在泄露的可能
在这里插入图片描述
因此当使用get()、set()这些方法时时,本线程会遍历ThreadLocalMap里面的Entry,把key为Null的Entry及value置为Null,这样下次GC就可以回收掉了,但如果本线程后续没有使用get()、set()怎么办呢?自然是无法回收了。

所以,我们在使用时,如有需要,应该显式的清除ThreadLocal中数据,即 mylocalVar.remove(), 从本线程种清除该entry

另一个点:threadLocalAge一般建议是类变量,成员变量亦可,但可能导致创建多个threadLocalAge实例,不影响使用,因为ThreadLocal变量,以obj.get() obj.set()为方法,所以存取时只要是同一个对象就行,多对象不过多占用了一些内存

另外使用线程池时,线程会复用,使用get()会获取到上次的值。需要特别注意及时清理

五、inheritableThreadLocals

我们上面说的内容其实主要是Threadlocal.ThreadLocalMap 这个类型的结构。然而在Thread中,使用这个map 结构的却有两个字段

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

其中我们对threadlocal做设置和访问时,其实使用到的是上述的 threadlocals 属性,那另一个map,即inheritableThreadLocals 字段又是干什么用的呢?

顾名思义inheritable 就是可遗传的,在这里就是说明threadlocals无法遗传,而该inheritableThreadLocals 段的值能被子线程获取

我们把最开始的代码略微改动,new出InheritableThreadLocal,然后使用主线程赋值,子线程也进行读取和改动

public class ThreadlocalTest {
    static InheritableThreadLocal<String> mylocalVar = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        mylocalVar.set("ABCDE");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //打印本地变量
                System.out.println("thread1 val: " + mylocalVar.get());
                mylocalVar.set("FGHIJ");
                System.out.println("thread1 val: " + mylocalVar.get());
            }
        });
        t1.start();
        Thread.sleep(2000);
        System.out.println("main thread val: " + mylocalVar.get());
    }
}

结果没有意外,主线程,和new出来的子线程最开始数据是一样的,相当于一个数据的两个备份,但是要注意的是:除了子线程创建的瞬间,子线程复制了父线程的一个备份,后面父子线程的内容是互不影响的。

当然,能够继承的原理,就在于Thread对象的初始化了,我们用主线程建立其他线程的时候,会在其他线程对象初始化的时候,读取主线程的InheritableThreadLocal,并创建一个新对象,复制父线程的数据到自己的InheritableThreadLocal里

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战斧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值