栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

并发编程-基础篇五-ThreadLocal

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

并发编程-基础篇五-ThreadLocal

什么是ThreadLocal?

ThreadLocal主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程是相对隔离的,在多线程的情况下,防止自己的数据被别的线程修改。

ThreadLocal在面试中也有很多面试官喜欢问,很多人认为数据是存在ThreadLocal中的,key是当前线程。

首先这样是错误的,实际上java中线程共享机制,最重要的是Thread中的ThreadLocalMap,ThreadLocal并不重要,它只是一个”钩子“,数据实际上被存在ThreadLocalMap中,下面慢慢道来:

ThreadLocal的使用
public class Test01 {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal<>();
        threadLocal.set("lisi");
        String s = threadLocal.get();
        System.out.println(s);
        new Thread(new Runnable() {
            @Override
            public void run() {
                String s1 = threadLocal.get();
                System.out.println(s1);
            }
        }).start();
    }
}
结果:
lisi
null
ThreadLocal.set(T value)的key是Thread吗?

首先,先告诉大家,ThreadLocal并不保存数据,ThreadLocalMap的key也不是Thread。

那为什么会有人会认为key是当前线程呢?看看set()的源码


createMap(t, value),传入了一个Thread和value,很多人就会认为ThreadLocal内部有一个Map,key就是Thread了,但事实是这样的吗?

并不是,看逻辑,先不管map是哪里来的,if(map !=null) map.set(this,value);也就是把this作为了map的key,那么这个this是谁呢,很显然是ThrealLocal。

再来说说map是怎么来的?

答案就在createMap(t,value)中,t是当前线程:


原来是通过create()方法new了一个ThreadLocalMap,并做了初始化,把当前ThreadLocal对象作为了key,firstValue作为了value。

到这里我们至少知道了两点:

  1. 有一个map,来历还不知道,数据存在这里,key是当前ThreadLocal对象,而不是当前线程
  2. Map类型是ThreadLocalMap,被赋值给Thread中的一个字段threadLocals

接着来:

如何理解ThreadLocal是一个钩子?


从上图可以看出,Thread t1 实例化后,内部会有一个ThreadLocalMap,刚开始为null,当执行到

ThreadLocal tl1 = new ThreadLocal();
t1.set("你好");

ThreadLocal会作为一个钩子,尝试从Thread T1中勾出ThreadLocalMap,如果发现这个成员变量尚未赋值,则new ThreadLocalMap()并把map设置进去。特别注意,由于set()是ThreadLocal的方法,所以map.set(this, value)中的this显然是ThreadLocal tl1。

此时此刻,内存中有三个对象,Thread t1、ThreadLocal tl1、ThreadLocalMap map,其中ThreadLocalMap在Thread内部,是ThreadLocal塞进去的。

ThreadLocalMap底层是什么样子的呢?

既然有个map,那他的数据结构应该和HashMap很像,但是从源码上看,它并未实现map接口,而且它的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

大概就是这样了:

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

用数组是因为,我们开发过程中可以一个线程可以有多个ThreadLocal来存放不同类型的对象,他们都将存放在当前线程的ThreadLocalMap中,需要用数组接收。

至于Hash冲突,我们先看一下源码:

private void set(ThreadLocal key, Object value) {
           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)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

从源码可以看到,ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。

然后会判断,如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

if (k == key) {
    e.value = value;
    return;
}

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

以下是get的源码,是不是就感觉很好懂了:

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 Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等  if (e != null && e.get() == key)。
            while (e != null) {
                ThreadLocal k = e.get();
              // 相等就直接返回,不相等就继续查找,找到相等位置。
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
对象存放在哪里?

在Java中,栈内存归属于每一个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程可见,即栈内存是线程私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
那么是不是说ThreadLocal的实例以及数据存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。


结果:

这又是怎么传递的呢?

Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

public class Thread implements Runnable {
  ……
   if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ……
}

如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。

多个Thread与同一个ThreadLocal

为什么访问同一个threadLocal.get(),Thread1存入的值不会被Thread2取出来?
上代码:

ThreadLocal zixia = new ThreadLocal();

// 至尊宝
new Thread(()->{
	// 遇到紫霞,紫霞往他心里塞了眼泪
    zixia.set("泪水");
}).start();

// 孙悟空
new Thread(()->{
	// 遇到紫霞,紫霞往他心里塞了紫青宝剑
    zixia.set("紫青宝剑");
}).start();

一个线程中有一个ThreadLocalMap,至尊宝这个线程中ThreadLocalMap保存了已zixia为key,”泪水“为value的entry;孙悟空也在自己的线程中保存了一个entry,两个线程里面的东西都不一样,又怎么会串起来呢?

同一个Thread与多个ThreadLocal
ThreadLocal zixia = new ThreadLocal();
zixia.set("泪水");

ThreadLocal baijingjing = new ThreadLocal();
baijingjing.set("回忆");

一个Thread中有一个ThreadLocalMap,第一次遇到的ThreadLocal会帮他创建一个map塞进去,之后无论遇到多少个ThreadLocal,都是直接用那个map,并把自己作为key,存到map中。

多个Thread与多个ThreadLocal

ThreadLocalMap本质是每个Thread内部各存一份,互不干扰。Thread在遇到不同的ThreadLocal,可以把ThreadLocal自身作为key存入map或从map中取出value。

ThreadLocalMap和WeakReference(弱引用)

在java中有4中类型的引用:强、软、弱、虚。

  • 强引用不受GC的影响,除非引用全部切断,Student student = new Student();这就是强引用,当student=null,student对象会在下次GC时被回收。
  • 软引用对象会在内存不足触发GC时被回收(适用于高速缓存)
  • 弱引用是每次GC时都回收
  • 虚引用(堆外内存,比如zerocopy)

对于Map,每一个键值对被称为Entry,相信大家都知道。


为什么ThreadLocalMap的Entry要继承弱引用呢?

虽然Entry继承了WeakReference,但并不是说Entry本身是弱引用,而是Entry的key是弱引用:

于是问题由为什么ThreadLocalMap的Entry要继承弱引用转为ThreadLocalMap为什么要把key包装成弱引用。


如果ThreadLocalMap的key是强引用,那么只要线程存在,ThreadLocalMap就存在,而ThreadLocalMap是entry数组,对应的entry数组就存在,entry的key是ThreadLocal,即使我们在代码中显示的赋值threadlocal=null,告诉CG要回收该对象,由于上面的强引用存在,不会被回收的。

ThreadLocalMap与内存泄露

原本引入Weak Reference是为了解决多个强引用导致ThreadLocal对象无法回收的问题,但一个解决策略的引入往往伴随着新bug的产生。试想一下,当外部强引用都切断后下一次GC回收了ThreadLocal对象,此时Entry的key会变成什么?

当tl1变成null,ThreadLocalMap的Entrys变成下面这样:
● null : value1(Entry1)
● tl2 : value2(Entry2)
● tl3 : value3(Entry3)

这就导致了一个问题,ThreadLocalMap中的key是弱引用,在没有外部强引用时,下次GC会被回收,如果创建的ThreadLocal线程一直持续运行,那么这个entry数组中的value就有可能一直不会被回收,发生内存泄漏。

比如线程池中的线程,线程都是复用的,那么之前的线程实例处理完之后,处于复用的原因线程依然存活(线程复用就是将线程阻塞,线程中一直持有entry数组,导致不会被GC回收),最终导致内存泄漏。

怎么解决?

在代码的最后一行remove就好了:

ThreadLocal localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

实际上,ThreadLocalMap也发现了这个问题,它会在每次get/set时判断key,如果key为null,则把value也归置为null:

当这种策略有风险,因为它的前提是下一次使用时会把上一次的key为null的清除,如果在也不用,是不是仍然没有清除。

所以最保险的方法是,每次使用完毕都及时清除。
看一下remove方法:

《阿里巴巴开发手册》也是这样建议:

一般来说,用完立即移除是最好的,但实际编程时有些公司喜欢在拦截器中取出用户信息放入线程,对此个人建议可以在拦截器的preHandle()中set,在afterCompletion()中remove()。

封装ThreadLocal 为什么要封装ThreadLocal

原因有两点:

  1. 对于Thread,如果希望在Interceptor中存入UserInfo并在Service层通过ThreadLocal把UserInfo钩出来,必须保证Interceptor和Service此时用的是同一个ThreadLocal。

我们可以创建一个ThreadLocalUtil中new一个ThreadLocal对象作为成员变量,就可以在service中取出来了:

public class ThreadLocalUtil{
	private ThreadLocalUtil(){
		};
	private static final ThreadlLocal> ZI_XIA  = new ThreadLocal();

}


2. 原生的ThreadLocal无法满足复杂的业务场景
比如现在我封装了一个最简单的ThreadLocal(装饰者模式,为的是解决第一个问题):

package com.example.czy.util;

public class MyThreadLocal {

    public MyThreadLocal(){

    };

    private static  final ThreadLocal THREAD_LOCAL= new ThreadLocal<>();

    public static void put(Object object){
        THREAD_LOCAL.set(object);
    }

    public static Object get(){
        return THREAD_LOCAL.get();
    }

    public static  void delete(){
        THREAD_LOCAL.remove();
    }
}
 

MyThreadLocal确实解决了第一个问题,复用了ThreadLocal,保证了Interceptor和Service用到的ThreadLocal是同一个对象。



但是,有两个缺陷:
● 无法存取多个不同的值
● 语意不明

比如,Service层希望往ThreadLocal里再添加一个Score对象,好让DAO层能获取到。你要怎么做?

ThreadLocalUtil
public class ThreadLocalUtil {
    public ThreadLocalUtil(){

    }

    
    private static final ThreadLocal> THREAD_LOCAL = new ThreadLocal<>();


    
    public static  void put(String key , Object o){
        //从ThreadLocal取出数据
        Map map = THREAD_LOCAL.get();
        //判断是否为空
        if (map==null){
            map = new HashMap<>();
            //把map放入到ThreadLocal中
            THREAD_LOCAL.set(map);
        }
        //在map中存放数据
        map.put(key,o);
    }

    
    public static Object get(String key){
        Map map = THREAD_LOCAL.get();
      return map!=null?map.get(key):null;
    }

    
    public static  void remove(String key){
        Map map = THREAD_LOCAL.get();
        map.remove(key);
    }
    
    public static void clear(){
        THREAD_LOCAL.remove();
    }


}

spring对ThreadLocal的封装

比如编写AOP日志时,经常会用到的RequestContextHolder,其实内部也维护了ThreadLocal。



那么Spring是如何做到remove的呢?使用过滤器(我们使用了拦截器)。

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号