Java中的线程本地变量ThreadLocal
Posted 攻城狮Chova
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的线程本地变量ThreadLocal相关的知识,希望对你有一定的参考价值。
线程本地变量ThreadLocal
基本概念
- 在处理多线程中最常用的方法就是使用锁,通过锁来控制多个不同的线程对临界区的访问,但是锁会在并发冲突时对性能造成影响,这是可以使用线程本地变量ThreadLocal避免锁竞争导致的并发冲突
- ThreadLocal的变量只有当前自身线程可以访问,其余线程无法访问,这样可以避免线程竞争
- ThreadLocal提供的线程安全方式不是在发生并发线程冲突时解决冲突,而是彻底避免的线程冲突的发生
ThreadLocal的原理
get
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
/*
* 每个线程自身都有一个ThreadLocalMap
* ThreadLocalMap中保存着所有的ThreadLocal变量
*/
ThreadLocalMap map = getMap(t);
if (map != null) {
/*
* ThreadLocalMap的key就是当前ThreadLocal对象的实例
* 多个ThreadLocal变量都保存在这个ThreadLocalMap中
*/
ThreadLocal.Entry e = map.getEntry(this);
if (e != null) {
// ThreadLocalMap中取出的值就是本地变量ThreadLocal变量
@SuppressWarnings("unchecked")
T result = e.value;
return result;
}
}
// 如果ThreadLocalMap没有进行初始化,就在这里对ThreadLocalMap进行初始化
return setInitialValue();
}
ThreadLocalMap
- ThreadLocal变量保存在每个线程的map中,这个map就是Thread对象中的threadLocals字段
ThreadLocal.ThreadLocalMap threadLocals = null;
- Thread.ThreadLocalMap是一个特殊的Map, 其中每个Entry的key都是一个弱引用:
- 当这个变量不再被其余的对象使用时,可以自动回收这个ThreadLocal对象,避免可能存在的内存泄漏问题
- 注意: 此时Entry中的key依旧是一个强引用,仍然可能存在如何回收,内存泄漏的问题
static class Entry extends WeakReference<ThreadLocal<?>> {
/**
* The value associated with
*/
Object value;
// 弱引用的key
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的使用
创建
private ThreadLocal<Integer> local = new ThreadLocal<>();
- ThreadLocal是一个泛型类,需要指定ThreadLocal的变量类型
赋值
local.set(6)
获取
local.get()
初始化
- ThreadLocal本地变量赋值时只有当前线程可见,所以无法通过其余线程进行赋值初始化
- 如果需要统一初始化所有线程的ThreadLocal变量的值,需要使用ThreadLocal类中的withInitial() 方法,此时ThreadLocal变量的值是对所有线程是可见的
private ThreadLocal<Integer> local = ThreadLocal.withInitial(() -> 6);
ThreadLocal的问题
内存泄漏
- ThreadLocal的ThreadLocalMap中的Entry的key是弱引用,当不存在外部强引用时 ,key就会被垃圾回收器自动回收
- ThreadLocal的ThreadLocalMap中的Entry的value依旧是强引用,这个value的引用过程如下:
Thread -> ThreadLocalMap -> Entry -> value
- 从value的引用过程中可以看出,只有当线程Thread被回收时 ,ThreadLocal中ThreadLocalMap才可能被回收. 只要线程不退出 ,value总是会存在一个强引用
- 对于线程池来说,大部分线程会一直存在于系统的整个生命周期中,这样会导致value始终都会存在一个强引用,造成value对象存在内存泄漏的可能
- 解决:
- 在ThreadLocalMap进行set(),get() 和remove() 操作的时候,对value值进行清理
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) // 如果获取到key,直接返回Entry return e; else // 如果未获取到key,就尝试清理.如果访问的key总是存在,就不会进入这个方法 return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { // Entry是一个弱引用 ThreadLocal<?> k = e.get(); if (k == key) // 如果获取到key,就返回Entry return e; if (k == null) // 如果key值为null,说明弱引用已经被回收.这时对value值进行清理回收 expungeStaleEntry(i); else // 如果key值不是想要找的key,说明存在Hash冲突.在这里对冲突进行处理,寻找下一个Entry i = nextIndex(i, len); e = tab[i]; } return null; }
- 这里可以看出,对value值进行清理回收的方法为expungeStaleEntry() 方法.在remove() 和set() 方法中,都会直接或者间接调用这个回收方法对value值进行清理
- 总结:
- 在ThreadLocal中,为了避免内存泄漏,不仅使用了弱引用对key进行维护,还会在每个操作上检查key是否被回收,对value值进行清理回收
- 因为ThreadLocal中的get() 方法总是会访问固定的几个一直存在的ThreadLocal, 这样会导致对value的清理回收动作一直不会执行.如果不手动调用set() 或者remove() 方法,依旧会存在内存泄漏的问题
- 所以当不需要某个ThreadLocal变量时,应该主动调用remove() 方法,避免产生内存泄漏问题
Hash冲突
- HashMap和ThreadLocalMap中Hash冲突处理的比较:
- HashMap:
- 使用链表法解决Hash冲突
- 冲突中的每一个槽值中都一个链表,冲突的元素构成一个链表,放置在同一个槽位中
- ThreadLocalMap:
- 使用线性探测法解决Hash冲突
- 如果放置的元素和槽值存在冲突,就选择下一个槽位存放
- HashMap:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 获取数组中Hash的一个位置
int i = key.threadLocalHashCode & (len-1);
/*
* 如果e == null,说明这个位置没有被占用,也就是没有冲突,不需要处理冲突,就不会进入循环
* 可以直接使用这个位置
* 如果e != null,说明发生冲突,就要进入循环中处理冲突,一直向下找,直到找到一个没有被占用的位置
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal的继承
InheritableThreadLocal
- 有些数据需要进行父子线程之间的传递,比如存在ThreadLocal的父线程开立一个子线程,需要在子线程中访问主线程中的ThreadLocal对象
- 因为在子线程中没有ThreadLocal对象,但是希望子线程可以获取到父线程的ThreadLocal对象,这里就可以使用InheritableThreadLocal对象替代ThreadLocal对象
- 使用InheritableThreadLocal后,子线程就可以访问父线程中的ThreadLocal对象,但是要注意以下两点:
- 变量的传递是发生在线程创建过程中,如果不是创建线程,而是使用线程池中的线程,子线程仍然无法获取到父线程中的ThreadLocal对象
- 变量的传递就是从父线程的map中复制给子线程,两个线程的value是同一个对象.如果这个value对象本身是线程不安全的,那么两个线程中的value同样也是线程不安全的
以上是关于Java中的线程本地变量ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章
Java并发机制--ThreadLocal线程本地变量(转)
Java并发多线程编程——ThreadLocal(线程本地存储)