0%

ThreadLocal

简介

ThreadLocal是线程局部变量。这些变量不同于它们的正常对应变量,因为每个访问一个(通过它的 get 或 set 方法)的线程都有它自己的、独立初始化的变量副本。

只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)

原理

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用ThreadLocal实例,value是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value

ThreadLocal内部有一个内部类,名为ThreadlocalMap,ThreadlocalMap中又有一个内部类名为Entry,Entry类定义如下:

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Entry继承WeakReference类,通过ThreadLocal<?>来当key值,

ThreadlocalMap中包含Entry数组,实现了Map的功能,Threadlocal中的threadLocalHashCode和数组容量减一的与运算来获得当前Threadlocal在Entry中下标。关于threadLocalHashCode如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  
/**
*ThreadLocal对象们依赖于附加到每个线程(Thread.threadLocals 和inheritableThreadLocals)的每线程线性探针哈希映射。
*ThreadLocal 对象充当键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在 ThreadLocalMaps 中有用),
*它消除了在相同线程使用连续构造的 ThreadLocals 的常见情况下的冲突,同时在不太常见的情况下保持良好行为
*/
private final int threadLocalHashCode = nextHashCode();


//下一个要给出的哈希码。原子更新。从零开始。
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* 连续生成的哈希码之间的差异 - 将隐式顺序线程本地 ID 转换为接近最佳传播的乘法哈希值,用于二次幂大小的表
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每个线程内部都有一个ThreadLocalMap的局部变量,用于保存自己线程中的Threadlocal变量,定义如下:

1
ThreadLocal.ThreadLocalMap threadLocals = null;

set流程

当我们新建了Threadlocal对象,并进行set时,

1
2
3
4
5
6
7
8
9
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

首先获得当前线程的threadLocals对象即ThreadLocalMap,如果map为空则新建,否则直接进行set

map初始化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//INITIAL_CAPACITY默认大小为16
table = new Entry[INITIAL_CAPACITY];
//当前threadlocal的哈希码与Entry数组大小进行与运算获得应该存储的下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

内存泄漏

内存泄露

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,

广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

强引用与弱引用

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal的内存泄露?什么原因?如何避免?

ThreadLocal 内存泄漏的原因

从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

为什么使用弱引用而不是强引用??

key 使用强引用

当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

总结

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。