多线程热知识(一):ThreadLocal简介及底层原理

ThreadLocal简介

那么首先,我们需要了解一下什么是ThreadLocal呢?以下是官方文档对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).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

此类提供线程局部变量,这些变量不同于普通变量,因为访问这些变量的每个线程(通过get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将类中的私有静态字段与线程(例如,用户ID或事务ID)关联的。

只要线程处于活动状态且ThreadLocal实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,其线程本地实例的所有副本都将接受垃圾收集(除非存在对这些副本的其他引用)。

转换成通俗的话来说就是,ThreadLocal对每个线程都保存了一份单独的变量副本,这样不同线程都只需对自身变量操作。从而避免了需要进行同步等复杂的步骤。(很豪横的思想,只要每个线程都有变量,不就不用做同步那些复杂的操作了么?!

了解完ThreadLocal是什么了,那么什么情况下适合使用ThreadLocal进行开发呢?

基于ThreadLocal本身的特性,其主要适用于一些多线程下修改变量,但不希望变量间互相影响的场景。

举项目中最常见的一个用法,就是用于保存前端请求的请求头信息

因为通常情况下,每次前端的请求服务器都会新起一个线程用于处理及传输,那么此时请求中的header头信息,肯定是只属于这个线程的,基于这个点,很容易就能和Thread Local本身以线程进行隔离的特性结合起来。

这样的话,能保证每个线程拿到的都会是自己对应的前端请求的请求头信息。

当然还有一些别的场景,比如如何区别当前请求是压测请求还是正常请求,也都可以采用ThreadLocal进行实现。

ThreadLocal原理分析

那么,ThreadLocal底层又是如何实现的这样的结构以支持其针对每个线程都能有其唯一的变量呢?接下来就由我来带你们一看源码底层是如何实现的。

get方法

首先我们来看get方法,其源代码如下所示:

public T get() {
  Thread t = Thread.currentThread(); // 获取当前线程
  ThreadLocalMap map = getMap(t); // 取到对应线程的ThreadLocalMap
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this); // 从这个map中取到对应的Entry结构
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value; // 泛型编程,转换成对应需要的类型。
      return result;
    }
  }
  return setInitialValue();
}

可以看到,比较关键的点就是ThreadLocalMap及其对应的Entry结构。那么这两个又是什么东西呢?

static class ThreadLocalMap {
  static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      // value 对应的其实是Thread在该ThreadLocal下的值。
      value = v; 
    }
  }
    ...... 
    
  private Entry[] table; 
  
    ......

可以看到,实际上ThreadLocalMap就是一个Entry数组。而每个Entry对应的是线程在一个ThreadLocal下的值。这样设计的原因就是为了充分兼顾一个线程可能存在的多个ThreadLocal的情况。具体图示如下所示:

那么如此一来,我们就可以梳理下从ThreadLocal中获取值的逻辑了:

1、获取到当前线程Thread

2、根据Thread获取到对应的ThreadLocalMap

3、从ThreadLocalMap中再获取当前ThreadLocal对应的Entry,并返回Entry中的值即可。

内存泄露

需要注意的一点小细节是,ThreadLocalMap中的Entry对象都是采用的弱引用。而这可能会导致一个比较严重问题,产生内存泄露。

为什么会产生内存泄露呢?这里我先借用一个Threadlocal引用的大致图解:

可以看到如果此时外界对ThreadLocal的引用取消了,那么此时ThreadLocal因为是弱引用,就会被GC进行回收,从而,我们就再也没有办法获取到对应的value值了,从而造成了内存泄露。

那么为什么要使用弱引用呢?改成强引用是否能解决问题呢?假设ThreadLocal使用的是强引用,那么当对ThreadLocal的引用被取消后,ThreadLocal由于依旧被ThreadLocalMap所指向,因此没法进行回收。

那么还是会造成内存泄露的问题,甚至泄漏的更“多”。

那么明显本质上,弱引用并不是引起内存泄漏的原因。实际上,要避免出现内存泄漏,还是需要我们线程主动的去"切断"引用。也即是在每次使用完后,都去调用相应的remove方法进行清除。

set方法

再看set方法,其实逻辑也是类似的。源代码我先放在这里了:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

可以看到也是

1、首先获取当前线程Thread。

2、获取到map对应的ThreadLocalMap对象。

3、判断当前ThreadLocalMap是否已经被创建,如果已经创建则往其中塞入值(key是ThreadLocal,value就是我们set的值)。

4、否则的话进行map的初始化。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY]; // 初始化数组,长度默认16
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算对应的哈希下标
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY); // 设置扩容阈值
}

初始化的关键代码我也放在这里了~如果上面的知识你都能了解的差不多的话,那么这里的初始化也必然不在话下啦~。

ThreadLocal优劣分析

优点

1、针对于常见的加锁保证互斥解决多线程访问的问题,ThreadLocal提出了新的思路,采用给每个线程都分配变量的方法解决冲突。性能相对较好。

2、各线程对于变量数据的修改是独立的,不会互相影响。

缺点

1、需要注意使用后调用remove方法清除对应的引用,避免出现内存泄漏的问题。

2、由于ThreadLocal作用的机理与线程是绑定的,因此不能直接用于保存异步任务的数据,需要通过一些别的方式进行操作。关于具体怎么实现,请期待我后续的文章哈哈哈~

如果你看到了这里,不妨给我点个赞、点个收藏,要是还能关注一下下就更好啦~

创作不易,感谢支持~

参考文献

弱引用什么时候被回收_面试官:ThreadLocal为什么会发生内存泄漏?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容