ThreadLocal技术分享(血泪教训)

学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么。所以我们先来看看ThreadLocal对我们到底有什么用,然后再来看看它的实现原理。

ThreadLocal如果单纯从名字上来看像是“本地线程"这么个意思,只能说这个名字起的确实不太好,很容易让人产生误解,ThreadLocalVariable(线程本地变量)应该是个更好的名字。我们先看一下官方对ThreadLocal的描述:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

1、每个线程都有自己的局部变量

每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释)

2、独立于变量的初始化副本

ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。

3、状态与某一个线程相关联

ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要

什么时候用用到:

举几个例子说明一下:

1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;

2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统但不正确的分析一次 web 请求的过程:

  • 用户在浏览器中访问 web 页面;
  • 浏览器向服务器发起请求;
  • 服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
  • 最后服务器将请求结果返回给客户端浏览器。

从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;

4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

现在我们先来看一段代码:

image

运行结果:

image

这个例子告诉我们 每一个线程之间的变量是互相之间不影响。

接着我们再来看一个例子:

image

输出结果:

image

咦,为什么这个每一个数值不一样呢。不是说好的 互不影响吗?

这时候就要拿出我多久不动的画笔,来给你们解析下为什么会出现这个情况。

我们先来看下一下 这个Demo1和Demo2的区别。

Demo1:

image

Demo2:

image

问题来了,Demo1每一次返回的都是0一个基本类型。但是indexnum是一个对象。所以每一次指向的还是同一个对象,为了加深理解 我们画一幅图来表示下。

image

所以ThreadLocal只保存了对象的地址副本,我们初始化的对象都是指向同一个地址,所以就会有这样子的 问题。那我们怎么解决这个问题呢。其实很简单

image

只需要每一次new 一个新的对象就好了,这样子就不会指向同一个地址。

再来看输出结果:

image

接下来我们看一下内部的源码 小戴带你读 ThreadLocal源码

image

ThreadLocal里面有几个方法 最主要的就是

public T get() { }

public  void set(T value) { }

public  void remove() { }

protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法

1:get方法:

image
  1. 先获取到当先的线程
  2. 判断当前线程中是否包含了ThreadLocalMap
  3. map 是 null 或者 map中的本地变量是空的话 就去创建初始值
image

创建初始值的时候又再去判断map是否存在,

  1. 不存在的话初始化map并setmap的key和value
  2. 存在的话直接set map的key和value
image

这就是ThreadLocal的get操作。

2:set方法:

image

也很类似 先获取当前线程,然后从当前线程的ThreadLocalMap中set值 存在则直接set不存在则初始化并set值

3:remove方法:

image

获取当前线程,当前线程实例就是该map的key 获取并remove掉。

这个就是ThreadLocal常用的三个方法。

接下去我们再来了解下ThreadLocal中的ThreadLocalMap

image

通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

image

那有人会问为什么要使用弱引用,其实这跟java 的设计思想有关系,java一直提倡的是弱化指针管理。所以就采用了key是若引用。那我们来对比下两种情况

  1. key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  2. key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用引起的。

综上所述

我们再使用ThredLocal的时候,使用完毕都要调用下remove方法。清除数据。

血泪教训!!!

p3事故

还记得那时3月26号的那一天,我不会忘记。那天中午睡醒之后,小伙伴带着我去luckin coffee buy a bottle of coffee,美滋滋。又是一个喝着咖啡敲着代码的日志。这时候内部技术群突然反馈,商家登录信息串号了。透,脑瓜子嗡嗡的~这个时候我在想我们也没有改动代码为什么会出现这样的问题呢。

这个时候,我意识到问题的严重性,马上飞奔回办公室,此时故障时间已经超过5分钟。我打开ci界面,马上回滚,还好merchant发布的快,5分钟内都回滚完毕。通知运营群让商家刷新页面,故障得以恢复。

其实在回滚的时候我已经意识到问题出在哪里(ThreadLocal使用后没有清除),随即出现在我脑海里的是,为啥以前没有出现问题现在就导致了这个问题的发生。后定位到 spring包版本的升级导致了aop的执行顺序:

image

我们代码里做了一个兜底方案,就是通过aop都去删除当前使用的ThreadLocal信息。但是spring包版本升级之后,aop先执行了这个兜底方案,然后又去执行了其他使用ThreadLocal的场景。

导致清除完之后,又去执行了Get操作,导致其他用户访问,拿到了其他线程池里面的用户信息(tomcat线程池复用)

后续针对这种情况做了兜底方案的改正。通过filter去实现兜底方案 因为执行顺序是

before:

aop 没有指定order所以没有办法处理。

after:

最外设置一个filter 里面设置和移除ThreadLocal

image
image

在最外层配置filter,ThreadLocal设置只放在Filter里面进行,方法执行完毕后,在filter中清除ThreadLocal。

ps:不管有没有这层兜底,使用了ThreadLocal之后 都在在final代码块中revome掉。

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

推荐阅读更多精彩内容