Java | ThreadLocal类

参照

全文参照视频:全面解析ThreadLocal

一、ThreadLocal说明

1. 作用

你可以理解你要操作的变量依托在了ThreadLocal上,一个变量对应一个ThreadLocal,变量依托在ThreadLocal时,会在每个线程中创建这个变量的副本,这样每个独立的副本作为该线程的全局变量独立使用,从而保证了多线程的安全性。

ThreadLocal<String> threadLocalOld = new ThreadLocal<String>(){
  @Override
  protected String initialValue() {
    return new String("dsadsa");
  }
};
Thread thread0 = new Thread(()->{
  String s0 = threadLocalOld.get();//每个线程调度get函数获取本线程的副本。
  // do sth
  threadLocalOld.set("dsad");// set函数set的值,只会设置本线程的值,不会对其他线程有任何影响。
});
Thread thread1 = new Thread(()->{
  String s0 = threadLocalOld.get();//获取的仍然是初始化的值"dsadsa",而不是"dsad";
});

2. 疑问

为什么不直接为每个线程都建一个变量?
因为你可能不知道到时候使用这个变量的线程会有多少个,即使你知道,比如有10个线程,几千个变量这些线程在本线程内独立使用,那是否你需要建立几万个这样的变量,而依托于ThreadLocal,需要多少个这样的变量,你建多少个就行了。

3. 变化

依托在今后对变量操作上会存在哪些变化呢?

  1. 修改变量时,用ThreadLocal的set方法;
  2. 获取变量时,用ThreadLocal的get方法;
  3. 初始化赋值时,创建ThreadLocal对象时,就重写其中的initialValue方法。
  4. 删除这个变量时,用ThreadLocal的remove方法。那么ThreadLocal对象就会被删除,对应的值也会被删除。

4. 从原理上来解释

  1. 有个类叫ThreadLocalMap,它是ThreadLocal的内部类,Thread类中的成员变量定义有ThreadLocalMap,你可以将其理解为Map集合,键是ThreadLocal类型,值就是你要操作的变量。
  2. ThreadLocal中的核心方法都是针对本线程中维护的ThreadLocalMap对象中以本ThreadLocal对象为键的键值对(entry)的操作。
  • set就是改变了这个entry的值;
  • get就是获取这个entry的值;
  • remove就是删除这个entry;
  1. 每个线程中维护的成员变量ThreadLocalMap存储着很多不同ThreadLocal对象为键的键值对,在不同的线程中相同变量对应的ThreadLocal对象是同一个,但是由于存储在不同线程维护的ThreadLocalMap中,ThreadLocal对象的那些方法又是针对本线程ThreadLocalMap操作(获取正在运行的线程,然后获取对应的ThreadLocalMap)的,所以做到了多个线程用同一个ThreadLocal对象,每个线程生成了独立的变量副本,操作也只对本线程的功能,具体源码参照第三块。

5. 与Sychrosize比较

  1. ThreadLocal相当于是在每个线程中新建了一个独立的变量,相同的只是ThreadLocal对象,而实际操作的变量值另外有独立副本。Sychrosize并不会建立副本,而是在时间上隔离了各个线程对这个变量的操作。
  2. ThreadLocal无法让线程间共享这个变量,共享的只是ThreadLocal对象本身,但多个线程并不会对ThreadLocal对象写操作,所以ThreadLocal对象的安全性无需考虑。

6. 使用场景

满足以下场景:假设存在A类,一串程序代码,多个线程在执行这个程序,比如多个用户同时在访问一个服务器的相同业务。

  1. A类对象在代码中不同地方都用到,且这些地方不连通,比如在不同方法,不同类中用到。(这就说明不能直接手写创建。)
  2. 需要保证A类对象在不同地方用的是同一个,也就是说对象只能创建一个,另外不能通过传参的方式传递。(好像可以用单例模式)
  3. 多个线程执行这个程序时,关于A类,每个线程都有自己的A类对象,也就是多个线程不能使用同一个A类对象;(这就说明不能光用单理模式)

分析:
可能有的人想到用单例模式,单例模式确实可以做到程序中的任何地方引用都是同一个对象,但是单例模式创建的变量是公共的,你写单例模式怎么写的就知道了。所以要怎么做到一个类的对象在单个线程内多处用的是同一个,但在多线程中是不同的呢?
答案就是将这个对象归为线程所有,就像静态变量归为类所有一样,将这个变量归线程所有,这样只要这个线程在运行那么获取的就是同一个对象,除非这个对象被线程删除了。那么就用到ThreadLocal类了。ThreadLocal会给当前线程的ThreadLocalMap成员变量赋值,并自己为键,A类对象为值来存在这个类似Map的集合中,这样就相当于把这个类的对象作为了Thread的成员变量。这样就可以实现通过公共的ThreadLocal对象让多个线程使用各自的A类对象。

二、ThreadLocal内部结构

1. 旧jdk版本

每个 ThreadLocal都创建一个Map(ThreadLocalMap),然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。

在这里插入图片描述

2. JDK1.8

(1)每个Thread线程内部都有—个Map(ThreadLocalMap),可以在Thread里面查看是一个ThreadLocalMap类型的成员变量,默认值为null。
(2)Map里面存储Threadlocal对象(key)和线程的变量副本( value)。
(3)Thread内部的Map是由ThreadLocal堆维护的,由Threadlocal负责向map获取和设置线程的变量值(后面祥讲)。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
ThreadLocalMap是ThreadLocal的一个内部类,在Thread的条件里面

在这里插入图片描述

3. 后者优势

  1. 每个Map存储的Entry数量变少;
  2. 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用;

三、ThreadLocal 核心方法源码分析

视频地址:ThreadLocal 核心方法源码

方法声明描述
protected T initialValue()返回当前线程局部变量的初始值,这个方法并非像构造方法,他只是个普通方法
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前程绑定的局部变量

1. public void set(T value)

总结:

  1. 首先获取当前线程,并根据当前线程其管理的ThreadLocalMap对象(Thread中的成员变量)
  2. 如果获取的ThreadLocalMap不为空,则将参数设置到ThreadLocalMap中(当前 Threadlocal的引用作为key,value参数为值)
  3. 如果ThreadLocalMap为空,则给该线程创建同样参数设置的ThreadLocalMap对象,赋值给当前线程管理的ThreadLocalMap
/*合设置当前线程对应的 Threadloca的值
合@ param value将要保存在当前线程对应的 Threadloca的值*/
public void set(T value){
//获取当前线程对象
Thread t= Thread.currentThread();
//获取此线程对象中维护的 ThreadLocalMap对象
ThreadLocalMap map= getMap(t);
//判断map是否存在
if(map != null)
//存在则调用map.set设置此实体 entry
map set(this, value);
else
//1)当前线程 Thread不存在 ThreadLoca lMap对象
//2)则调用 createMap进行 ThreadLoca IMap对象的初始化
//3)并将t(当前线程)和 value(t对应的值)作为第一个 entry存放至 ThreadLoca lMap中
createMap(t, value);
}

public void set(T value) {
//获取当前线程对象
    Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
//判断map是否存在
    if (map != null)
    //存在则调用map.set设置此实体entry
        map.set(this, value);
    else
    //1)当前线程Thread不存在 ThreadLocalMap对象
	//2)则调用createMap进行 ThreadLocalMap对象的初始化
	//3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        createMap(t, value);
}

/*
获取当前线程 Thread对应维护的 Thr eadLoca IMap
@param t 当前线程
Reture the map对应维护的ThreadLocalMap
ThreadLocalMap作为Thread里的一个成员变量在被维护
*/
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//新建一个ThreadLocalMap,赋值给当前线程的threadLocals成员变量
 void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
 }
 

Thread类截图
在这里插入图片描述

2. public T get()

  1. 首先获取当前线程,根据当前线程获取一个Map
  2. 如果获取的Map不为空,则在Map中以 Threadlocal的引用作为key来在Map中获取对应的 Entry e,否则转到4
  3. 如果e不为nul,则返回 e value,否则转到4
  4. Map为空或者e为空,则通过 initialvalue函数获取初始值vaue,然后用 ThreadLocal的引用和vaue作为firstKeyi和 firstvaluet创建一个新的Map
    总结:先获取当前线程的 ThreadLocalMap变量,如果存在则返回值,不存在则刨建并返回初始值
public T get() {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t); 获取当前线程管理的ThreadLocalMap对象(成员变量)
        if (map != null) {
        //ThreadLocalMap对象不为null,根据当前ThreadLocal获取键值对
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
            //键值对不为null,返回对象的值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //ThreadLocalMap对象为null或者键值对为null则初始化
        return setInitialValue();
    }

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;
    }

3. public void remove()


 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程管理的ThreadLocalMap对象
     if (m != null)
         m.remove(this);//如果不为空就删除掉对应的键值对
 }

4. protected T initialValue()

  1. 这个方法是一个延迟调用方法,从上面的代码我们得知在set方法还未调用而先调用了get方法时才执行,并且一个线程仅执行1次。当然如果remove了,又get也会执行这个方法的,get的逻辑理解了,这个应该不难理解。
  2. 这个方法缺省实现直接返回一个null。
  3. 如果想要一个除null之外的初始值,可以重写此方法。(备注:该方法是一个 protected的方法,显然是
    为了让子类覆盖而设计的)
    protected T initialValue() {
        return null;
    }


下面内容供了解

四、ThreadLocalMap源码分析

从前面不难看出,ThreadLocal核心方法就是围绕着当前线程中维护的ThreadLocalMap对象(Thread中的一个局部变量)来操作的,ThreadLocalMap是ThreadLocal的一个内部类,类似于Map集合,存储着key(本线程的ThreadLocal<T>对象)—value(局部变量值T v)。

1. ThreadLocalMap结构图

在这里插入图片描述
说明:

  1. 在 ThreadLocalMap中,也是用Entry来保存KV结构数据的。不过Entry中的key只能是 ThreadLocal对象,这点在构造方法中已经限定死了。
  2. 另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将 Threadlocal对象的生命周期和线程生命周期解绑,什么意思见下面。

2. 从内存泄漏了解ThreadLocalMap

首先去了解一下内存泄漏和四种引用的概念,这里从内存泄漏的角度说明了ThreadLocalMap中key为什么要使用弱引用(A2引用)。也可以借此机会实际了解一下各种引用。

在这里插入图片描述
说明:上图中实线表示强引用,这里强弱讨论的是A2也就是entry中键对ThreadLocal对象的引用

1. 强/弱引用的分析

  1. 假设在业务代码中使用完Threadlocal, ref这个对ThreadLocal对象的引用被回收了,A1消失。
  2. 但由于Key对Threadlocal的引用(A2)未结束,所以Threadlocal对象不会被回收,由于B系列引用所以Entry不会被回收,这就造成了内存泄漏。
  3. 如果A2是弱引用,则Threadlocal会被回收,但同样的entry仍然被强引用,其中的value也不会被回收,也存在泄漏,但是好一点,至少回收了Threadlocal,key变成了null。
  4. 所以说要记得remove(),可以直接将Entry设置为null;

综上, ThreadLocal内存泄漏的根源是:由于ThreadLocalMap作为Thread的一个成员变量,其生命周期跟Thread一样长,线程不结束,B的引用链都存在。

为什么用弱引用呢?
事实上,在 ThreadLocalMap中的set/getEntry方法(对应ThreadLocal中set/get方法会有调用)中,会对key为null(也即是 Threadlocal为null)进行判断,如果为null的话,那么是会对vaue置为null。这就意味着使用完Threadlocal, CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次获取其它值时调用到ThreadLocalMap的set, get, remove中的任一方法的时,key为null的就会被清除,从而避免内存泄漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值