深入分析ThreadLocal

首先看下jdk里这个类的定义:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code 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).

翻译过来是:

1.该类提供了线程本地变量。这些变量与一般的变量不同,每个线程通过 get 和 set 方法来访问这个变量时都有自己独立的变量副本。

2.ThreadLocal实例建议定义为静态私有的。

先来看第一句,怎么理解呢,举个之前项目里遇到的问题做例子:

我们有一个接口传入参数里有时间戳类似这样(“20180404121212”),在代码里需要将这个字符串转换为 Date 类型,做校验。一开始我们代码是类似下面这样写的:

class A {

static SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");

public void func1(String time){

Date date = df.parse(time);

//其他逻辑...

}

}

正常跑是没问题的,但是压测的时候就遇到问题了,偶尔会有异常抛出来。我们来看下上面这段代码有什么问题呢:

由于SimpleDateFormat 是静态的(其实不是静态的也一样)并且class A是单例的,所以每次请求进入到func1方法里使用到的SimpleDateFormat 对象的实例都是同一个,也就是说,多个线程使用同一个变量,然而jdk自带的这个SimpleDateFormat是线程不安全的(具体为什么可以去网上搜搜相关文章很多,),所以就有了上面说的问题。

那么针对这个问题,我们当时采用了一种解决方法(可能并不是最好的,不要纠结,这里只是为了说ThreadLocal):

class A {

private static ThreadLocal threadLocal_df = new ThreadLocal() {

@Override

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("yyyyMMddHHmmss");

}};

public void func1(String time){

Date date = threadLocal_df.get().parse(time);

//其他逻辑...}

}

我们再看下threadlocal的第一句定义:

1.该类提供了线程本地变量。这些变量与一般的变量不同,每个线程通过 get 和 set 方法来访问这个变量时都有自己独立的变量副本。

我们上面遇到的问题是每个线程都是用同一个SimpleDateFormat对象,而SimpleDateFormat又是线程不安全导致的。那么我们自然的想到可不可以每个线程都使用自己的SimpleDateFormat对象来解决这个问题。

所以就有了上面的写法:定义一个ThreadLocal对象用来保存我们实际要用的SimpleDateFormat对象(保存这个描述其实是不对的,后面会深入讲threadlocal的原理),当要用时通过ThreadLocal对象的get方法,获取到每个线程自己的SimpleDateFormat对象,来进行后面的业务逻辑。

下面我们来看下ThreadLocal具体是怎么实现的这样的效果的:

先来说下ThreadLocal的实现原理:

1. 在ThreadLocal里面定义了一个内部类 ThreadLocalMap ,这个map就是一个hashmap(但是有些实现细节和jdk里的hashmap不同,比如rehash等)


ThreadLocal里面定义的 ThreadLocalMap  类

这个map就是用来保存我们要用的每个线程独立的变量,所以虽然这个类的定义在ThreadLocal里面,但是实际用的时候,是在Thread里面用的:


Thread 里面引用 ThreadLocalMap   的实例

这个map的key是ThreadLocal对象,value就是我们要用的变量。所以可以通过ThreadLocal的get方法得到实际的变量。借用网上的一张图来表示它们之间的关系:


然后我们来看具体源码,上面我们使用ThreadLocal时定义的时候有这么一段是干什么用的呢:

@Override

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("yyyyMMddHHmmss");

}

这里重写了ThreadLocal的方法initialValue,这个方法用来返回一个初始化的值。那么这个返回的变量在哪里用呢。


可以看到是get()方法里调用setInitialValue()然后再调用initialValue()方法。

get里面首先获取当前线程,然后从当前线程中取出map,在map里面查找当前实例做为key的value,如果不存在,说明是第一次使用,则调用setInitialValue()进行初始化,初始化其实就是把当前实例做key,写入到当前线程的map中。

关于这个ThreadLocalMap还有两个比较有意思的地方:

1.这个map每次发生hash碰撞时不是使用链式解决的,而是将当前hash值+1再尝试。


ThreadLocalMap碰撞解决通过hash+1方式

2.由于ThreadLocalMap 这个map 的key只可能时ThreadLocal对象,所以jdk里面对ThreadLocal的hash值也做了特殊的计算方法,每个ThreadLocal对象的hash值都是之前的加上一个固定值 0x61c88647 。这个固定值也是很神奇,按这种方式,发生碰撞的概率非常小,也就是散列的很均匀...(不知道是什么原理)


ThreadLocal的hash算法

关于ThreadLocal还有一块,就是内存泄漏的问题,先说结论:

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

关于内存泄漏这块,网上很多文章说是弱引用导致的,其实这是不对的,我们来看下jdk里面的解释:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

可以看到其实是为了解决某些ThreadLocal所对应的变量超级大,并且这个线程的存活时间很长,导致这个大变量即使不使用了也不会被释放,所以jdk才采用弱引用做key。

我们先假设key不是弱引用,还是用最开始的例子:

class A {

private static ThreadLocal threadLocal_df = newThreadLocal() {

@Override

protected SimpleDateFormat initialValue() {

return new SimpleDateFormat("yyyyMMddHHmmss");

}};

public void func1(String time){

Date date = threadLocal_df.get().parse(time);

threadLocal_df = null;

//其他逻辑...}

}

当我们把threadlocal主动改为null时,实际上这个threadlocal再后面并不会被回收,因为threadlocalmap还持有它的引用。

而如果这里使用了弱引用,当我们把threadlocal改为null时,这个threadlocal就只有被threadlocalmap通过弱引用引用。而按照弱引用的规则,这时候threadlocal是可以被回收的。

其实这个时候也只是保证key被回收,其实占用空间比较大的value还是没有被回收的,jdk这个时候还做了一件事:就是每次调用ThreadLocal的set,get方法时,都会检测一遍map里所有key为null的entry,然后释放它。

除此之外,其实jdk推荐的释放方法时通过调用ThreadLocal的remove方法,我们看下remove的实现:


如图,remove是调用了Entry继承自Reference的clear方法,将key置为null。然后又调用了expungeStaleEntry方法将value置为null。

所就有了结论:

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

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

推荐阅读更多精彩内容