深入分析ThreadLocal

2018-04-04  本文已影响0人  ooleaf123

首先看下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()方法,清除数据。

上一篇下一篇

猜你喜欢

热点阅读