ThreadLocal
ThreadLocal是什么
在处理多线程并发安全的问题中,我们最常使用的方法就是通过锁来控制多个不同线程对临界区的访问。
但无论是什么样的锁,乐观锁或者悲观锁,尽管JDK
在升级过程中对它们都有不同程度的优化,但在并发冲突的时候总是会对性能产生一定的影响。
而我们今天的主角 ThreadLocal
解决的正是彻底避免多线程之间产生的竞争问题。
从字面意思上看,ThreadLocal
可以解释为线程的局部变量,也就是说对于同一个 ThreadLocal
变量,每个线程访问的都是自己本地变量值,既然只有自己能够访问,那自然就避免了冲突。
所以说,ThreadLocal
相较于锁提供了一种与众不同的保证线程安全的方式,它不是在线程发生冲突时想办法解决冲突,而是彻底的避免了冲突的发生。
举一个现实生活当中的例子:
去商场购物时我们总会将购买的商品放到购物车中,并且商场会为每个人准备单独的购物车,如果所有人购买的东西都放到一个购物车,大家就会混淆各自购买的商品,最终结账也会出大问题。所以每个人都有属于自己的购物车,这样才不会各自混淆,ThreadLocal
的实现方式也和这个例子类似。
ThreadLocal使用
ThreadLocal
提供了一个 withInitial()
方法统一初始化所有线程的 ThreadLocal
的值,这里我们将 thread1
、thread2
和主线程的值都初始化为0:
1 |
|
执行结果:
1 |
|
我们可以看到,thread1
和 thread2
设置的值互不影响,由于主线程没有重新设置值,所以取得初始化的值0。
ThreadLocal的实现原理
ThreadLocal
变量是如何做到只对当前线程可见的呢?我们先从 ThreadLocal
类当中最基本的 get()
方法说起:
1 |
|
可以看到,所谓的 ThreadLocal
变量就是保存在每个线程的 ThreadLocalMap
中的。这个 map
就是 Thread
对象中的 threadLocals
字段。如下:
1 |
|
我们可以得出结论,最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是 ThreadLocalMap
的封装,传递了变量值。ThrealLocal
类中可以通过 Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
每个 Thread
中都具备一个 ThreadLocalMap
,而 ThreadLocalMap
可以存储以 ThreadLocal
为 key ,Object 对象为 value 的键值对。
1 |
|
比如我们在同一个线程中声明了两个 ThreadLocal
对象的话, Thread
内部都是使用仅有的那个ThreadLocalMap
存放数据的,ThreadLocalMap
的 key 就是 ThreadLocal
对象,value 就是 ThreadLocal
对象调用set
方法设置的值。
ThreadLocal
结构图如下所示:
ThreadLocal内存泄漏
ThreadLocal.ThreadLocalMap
是一个比较特殊的 Map
,它的每个 Entry
的 key
都是一个弱引用:
1 |
|
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value
是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key
会被清理掉,而 value
不会被清理。
这个 value
的引用链条如下:
可以看到,只有当 Thread
被回收时,这个 value
才有被回收的机会,否则,只要线程不退出,value
总是会存在一个强引用。但是,要求每个 Thread
都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成 value
对象出现泄漏的可能。
如此一来,ThreadLocalMap
中就会出现 key
为 null
的 Entry
。假如我们不做任何措施的话,value
永远无法被 GC
回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key
为 null
的记录。使用完 ThreadLocal
方法后最好手动调用 remove()
方法。
以 getEntry()
方法为例:
1 |
|
真正用来回收value的是 expungeStaleEntry()
方法,在 remove()
和 set()
方法中,都会直接或者间接调用到这个方法进行 value
的清理:
从这里可以看到,ThreadLocal
为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护 key
,还会在每个操作上检查 key
是否被回收,进而再回收 value
。
但是从中也可以看到,ThreadLocal
并不能100%保证不发生内存泄漏。
比如,很不幸的,你的 get()
方法总是访问固定几个一直存在的 ThreadLocal
,那么清理动作就不会执行,如果你没有机会调用 set()
和 remove()
,那么这个内存泄漏依然会发生。
因此,一个良好的习惯依然是:当你不需要这个 ThreadLocal
变量时,主动调用 remove()
,这样对整个系统是有好处的。
ThreadLocalMap中的Hash冲突处理
ThreadLocalMap
作为一个 HashMap
和 java.util.HashMap
的实现是不同的。对于java.util.HashMap
使用的是拉链法来处理冲突:
但是,对于 ThreadLocalMap
,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:
整个 set()
方法过程如下:
1 |
|
InheritableThreadLocal
在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的 ThreadLocal
对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:
1 |
|
执行上述代码,可以得到结果:
1 |
|
可以看到,子线程可以访问到从父进程传递过来的一个数据。虽然 InheritableThreadLocal
看起来挺方便的,但是依然要注意以下几点:
- 变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了。
- 变量的赋值就是从主线程的
map
复制到子线程,它们的value
是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题。
使用场景
- 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有
SimpleDateFormat
和Random
)。 - 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。