探秘Java中的ThreadLocal
探秘Java中的ThreadLocal
多线程基础
Thread
对象代表一个线程,我们可以在代码中调用Thread.currentThread()
获取当前线程
ThreadLocal
简介
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
用法
创建,支持泛型
1 | ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>(); |
set方法
1 | mStringThreadLocal.set("hello world"); |
get方法
1 | mStringThreadLocal.get(); |
源码
刚才的get与set方法的源码如下,两个方法都用到了方法getMap()
1 | public T get() { |
从getMap()方法可以看到,ThreadLocalMap是ThreadLocal的内部类,ThreadLocalMap的内部类Entry实际持有线程私有变量(TLS),源码如下:
1 | public class ThreadLocal<T> { |
可以看到,Thread类中有threadLocals变量(ThreadLocalMap类型),ThreadLocalMap是ThreadLocal的内部类
注意
如果使用了线程池,上一个线程设置了ThreadLocal,线程用完后不会被销毁,而是会回归线程池等待下一次分配,这时ThreadLocal可能就会污染,所以一般线程用完ThreadLocal后最后调用remove方法
1 | /** |
Inheritable ThreadLocal(ITL)
ThreadLocal是线程私有数据,线程之间无法传递,但ThreadLocal.java中提供了ITL,可以用于父子线程之间的上下文传递
源码
Thread类不仅有threadLocals变量,还有inheritableThreadLocals变量(它也是ThreadLocalMap类型)
使用InheritableThreadLocal可以将某个线程的ThreadLocal值在其子线程创建时传递过去,在Thread类中创建线程时有特殊的处理逻辑
1 | //Thread.java |
局限性
线程不安全
Inheritable ThreadLocal并不是用来解决线程不安全的问题的,因此父子线程之间的修改会影响到其他线程
线程池中失效
使用线程池要及时remove
如果使用了线程池,则Thread、Inheritable ThreadLocal变量都要remove,否则线程池回收后,变量还存在内存中(key是弱引用被回收,但value还在),导致内存泄露
一些问题
Thread, ThreadLocal, ThreadLocalMap的关系
- ThreadLocalMap 是 ThreadLocal 的内部类
- ThreadLocalMap 的 key 是 ThreadLocal,value是 TLS
- Thread 有 ThreadLocal.ThreadLocalMap 成员变量,set 方法会 createMap
ThreadLocalMap的底层结构?数组?链表?哈希表?
- ThreadLocal 的数据结构是个环形数组
- ThreadLocalMap 维护了 Entry 环形数组,数组中元素 Entry 的逻辑上的 key 为某个 ThreadLocal 对象(实际上是指向该 ThreadLocal 对象的弱引用),value 为代码中该线程往该 ThreadLoacl 变量实际塞入的值。
对象存放在哪里么?
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
内存泄露
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
get方法、set方法不能防止内存泄漏,remove可以
解决:在代码的最后使用remove就好了(参见上文的『注意』小节
为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
为什么 ThreadLocalMap 采用开放地址法来解决哈希冲突
- 因为开放地址法的优点:当节点规模较少,或者装载因子较少的时候,使用开放寻址较为节省空间
- ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间
- ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在 2 的 N 次方的数组里, 即 Entry[] table
C++的ThreadLocal
C++11引入了ThreadLocal,但其实现与原理与Java大相庭径
它会影响变量的存储周期(Storage duration),C++中有4种存储周期:
- automatic
- static
- dynamic
- thread
有且只有thread_local关键字修饰的变量具有线程周期(thread duration),这些变量(或者说对象)在线程开始的时候被生成(allocated),在线程结束的时候被销毁(deallocated)。并且每 一个线程都拥有一个独立的变量实例(Each thread has its own instance of the object)。thread_local 可以和static 与 extern关键字联合使用,这将影响变量的链接属性(to adjust linkage)。
以下三类变量可以被声明为thread_local:
- 命名空间下的全局变量
- 类的static成员变量
- 本地变量
引用《C++ Concurrency in Action》书中的例子来说明这3种情况:
1
2
3
4
5
6
7
8
9
10
11thread_local int x; //A thread-local variable at namespace scope
class X
{
static thread_local std::string s; //A thread-local static class data member
};
static thread_local std::string X::s; //The definition of X::s is required
void foo()
{
thread_local std::vector<int> v; //A thread-local local variable
}