ThreadLocal
Contents
ThreadLocal
- ThreadLocal 是什么
- 弱引用
- 避免内存溢出的操作
- 开放地址法解决 hash 冲突
- 各种内部类
线程本地变量有时会简写为 TLV, Thread Local Variables
ThreadLocal 是什么
ThreadLocal 是线程的局部变量, 也就是说这个变量是线程独有的。 通常情况下, 我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问, 其他线程则无法访问和修改。
变量是同一个, 但是每个线程都使用同一个初始值, 也就是使用同一个变量的一个新的副本, 这种情况下 TreadLocal 就非常有用。
应用场景: 当很多线程需要多次使用同一个对象, 并且需要该对象具有相同初始值的时候, 最适合使用TreadLocal。
事实上,从本质上讲,就是每个线程都维持一个 MAP,而这个map的key就是TreadLocal,而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那就肯定不存在线程安全的问题。总体来讲,TreadLocal这个变量的状态根本没有发生变化。它仅仅是充当了一个key的角色,另外提供给每一个线程一个初始值。如果允许的话,我们自己就能实现一个这样的功能,只不过恰好JDK就已经帮助我们做了这个事情。
使用TreadLocal维护变量时, TreadLocal为每个使用该变量的线程提供独立地变量副本, 所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,目标变量对象是线程的本地变量,这也是类名中Local所需要表达的意思。
TreadLocal的四个方法
void set(Object val),设置当前线程的线程局部变量的值 Object get () 返回当前线程所对用的线程局部变量。 void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,线程结束后,局部变量自动被GC Object initValue() 返回该线程局部变量的初始值,使用protected修饰,显然是为了让子类覆盖而设计的。
ThreadLocalMap
- HashMap 的数据结构是数组+链表
- ThreadLocalMap的数据结构仅仅是数组
- HashMap 是通过链地址法解决 hash 冲突的问题
- ThreadLocalMap 是通过开放地址法来解决 hash 冲突的问题
- HashMap 里面的Entry 内部类的引用都是强引用
- ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用
对象存放在哪里
在Java中, 栈内存归属于单个线程, 每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
问: 那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是,因为ThreadLocal实例实际上也是被其创建的类持有 (更顶端应该是被线程持有) 。而 ThreadLocal的值其实也是被线程实例持有。
它们都是位于堆上, 只是通过一些技巧将可见性修改成了线程可见。
Global && Local
上面的两个修饰看似矛盾, 实则不然。
Global 意思是在当前线程中, 任何一个点都可以访问到ThreadLocal的值。
Local 意思是该线程的ThreadLocal只能被该线程访问,一般情况下其他线程访问不到。
用法简介
创建,支持泛型
|
|
set方法
mStringThreadLocal.set(“droidyue.com”);
get方法
mStringThreadLocal.get();
|
|
ThreadLocal初始值
为ThreadLocal设置默认的get初始值,需要重写initialValue方法,下面是一段代码,我们将默认值修改成了线程的名字
ThreadLocal mThreadLocal = new ThreadLocal() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
Android中的应用
在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
static final ThreadLocal sThreadLocal = new ThreadLocal();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException(“Only one Looper may be created per thread”);
}
sThreadLocal.set(new Looper(quitAllowed));
}
如何实现
为了更好的掌握ThreadLocal, 我认为了解其内部实现是很有必要的,这里我们以set方法从起始看一看ThreadLocal的实现原理。
下面是ThreadLocal的set方法,大致意思为
首先获取当前线程
利用当前线程作为句柄获取一个ThreadLocalMap的对象
如果上述ThreadLocalMap对象不为空,则设置值,否则创建这个ThreadLocalMap对象并设置值
源码如下
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
下面是一个利用Thread对象作为句柄获取ThreadLocalMap对象的代码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面的代码获取的实际上是Thread对象的threadLocals变量,可参考下面代码
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
而如果一开始设置,即ThreadLocalMap对象未创建,则新建ThreadLocalMap对象,并设置初始值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结: 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。
对象存放在哪里
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
问: 那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是,因为ThreadLocal实例实际上也是被其创建的类持有 (更顶端应该是被线程持有) 。而ThreadLocal的值其实也是被线程实例持有。
它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
关于堆和栈的比较,请参考Java中的堆和栈的区别
真的只能被一个线程访问么
既然上面提到了ThreadLocal只对当前线程可见,是不是说ThreadLocal的值只能被一个线程访问呢?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。
如下,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
private void testInheritableThreadLocal() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set(“droidyue.com”);
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i(LOGTAG, “testInheritableThreadLocal =” + threadLocal.get());
}
};
t.start();
}
上面的代码输出的日志信息为
I/MainActivity( 5046): testInheritableThreadLocal =droidyue.com
使用InheritableThreadLocal可以将某个线程的ThreadLocal值在其子线程创建时传递过去。因为在线程创建过程中,有相关的处理逻辑。
//Thread.java
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
//code goes here
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/Stash the specified stack size in case the VM cares/
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
上面代码就是在线程创建的时候, 复制父线程的inheritableThreadLocals的数据。会导致内存泄露么
有网上讨论说ThreadLocal 会导致内存泄露,原因如下
首先ThreadLocal 实例被线程的 ThreadLocalMap 实例持有, 也可以看成被线程持有。
如果应用使用了线程池, 那么之前的线程实例处理完之后出于复用的目的依然存活, 所以,ThreadLocal设定的值被持有,导致内存泄露。
上面的逻辑是清晰的,可是ThreadLocal并不会产生内存泄露,因为ThreadLocalMap在选择key的时候,并不是直接选择ThreadLocal实例,而是ThreadLocal实例的弱引用。
static class ThreadLocalMap {
/**
The entries in this hash map extend WeakReference, using
its main ref field as the key (which is always a
ThreadLocal object). Note that null keys (i.e. entry.get()
== null) mean that the key is no longer referenced, so the
entry can be expunged from table. Such entries are referred to
as “stale entries” in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?» {
/* The value associated with this ThreadLocal./
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
所以实际上从ThreadLocal设计角度来说是不会导致内存泄露的。关于弱引用,了解更多,请访问译文: 理解Java中的弱引用
使用场景
实现单个线程单例以及单个线程上下文信息存储,比如交易id等
实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例
承载一些线程相关的数据,避免在方法中来回传递参数
注意: Android的ThreadLocal与Java实现略有不同,但是原理是一致的。
参考文章
Java ThreadLocal
Threadlocals and memory leaks in J2EE
Java Thread Local – How to use and code sample
ThreadLocal in Java – Example Program and Tutorial
Hash冲突怎么解决
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是: 每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
ThreadLocalMap的问题 ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的
由于Entry的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
为什么使用弱引用? key 使用强引用: 引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。 key 使用弱引用: 引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现: 由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障: 弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
如何避免泄漏 既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。 ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
魔数0x61c88647
魔数0x61c88647与碰撞解决 机智的读者肯定发现ThreadLocalMap并没有使用链表或红黑树去解决hash冲突的问题,而仅仅只是使用了数组来维护整个哈希表,那么重中之重的散列性要如何保证就是一个很大的考验 ThreadLocalMap通过结合三个巧妙的设计去解决这个问题:
- Entry的key设计成弱引用,因此key随时可能被GC (也就是失效快) ,尽量多的面对空槽
- (单个ThreadLocal时)当遇到碰撞时,通过线性探测的开放地址法解决冲突问题
- (多个ThreadLocal时)引入了神奇的0x61c88647,增强其的散列性,大大减少碰撞几率 之所以不用累加而用该值,笔者认为可能跟其找最近的空槽有关 (跳跃查找比自增1查找用来找空槽可能更有效一些,因为有了更多可选择的空间spreading out) ,同时也跟其良好的散列性有关 0x61c88647与黄金比例、Fibonacci 数有关,读者可参见What is the meaning of 0x61C88647 constant in ThreadLocal.java
https://stackoverflow.com/questions/38994306/what-is-the-meaning-of-0x61c88647-constant-in-threadlocal-java https://web.archive.org/web/20161121124236/http://brpreiss.com/books/opus4/html/page214.html
链地址法和开放地址法的优缺点
开放地址法
- 容易产生堆积问题,不适于大规模的数据存储。
- 散列函数的设计对冲突会有很大的影响, 插入时可能会出现多次冲突的现象。
- 删除的元素是多个冲突元素中的一个, 需要对后面的元素作处理, 实现较复杂。
链地址法, 链表法
- 处理冲突简单, 且无堆积现象, 平均查找长度短。
- 链表中的结点是动态申请的, 适合构造表不能确定长度的情况。
- 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
- 指针需要额外的空间, 故当结点规模较小时,开放定址法较为节省空间。
ThreadLocalMap 采用开放地址法原因
ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了 ThreadLocal 往往存放的数据量不会特别大 (而且key 是弱引用又会被垃圾回收,及时让数据量更小) ,这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
https://juejin.cn/post/6844903974454329358 来源: 掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://zhuanlan.zhihu.com/p/139214244
https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/
https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666
https://blog.csdn.net/Summer_And_Opencv/article/details/104632272
https://juejin.cn/post/6844903974454329358
Author -
LastMod 2017-03-24