总结
总结放前面防止太长不看:
- 每个线程都有一个threadLocals字段,是一个ThreadLocalMap的实例,所有的ThreadLocal代表的线程私有数据都存放在这里面,key对应的是一个ThreadLocal对象,而value就是线程私有的值。
- ThreadLocalMap对象的创建是懒惰的,第一次在该线程上用到ThreadLocal时才会创建,初始容量是
16
,threshold默认为2/3的数组大小
,如果元素个数超过threshold,清理一次无用的Entry(key被回收掉的Entry),如果清理一次之后元素个数仍大于3/4*threshold
,进行扩容(翻倍) - ThreadLocalMap通过线性探测法解决冲突。
- ThreadLocalMap通过弱引用指向一个ThreadLocal对象(key),强引用指向该线程对应的值(value)
- ThreadLocalMap对象的创建是懒惰的,第一次在该线程上用到ThreadLocal时才会创建,初始容量是
- key->ThreadLocal对象是弱引用,代表ThreadLocal对象一旦只剩下线程中ThreadLocalMap中key的引用,下一次GC的时候就会被回收。但该Entry和内部的value仍会占用空间,ThreadLocalMap中的许多方法都在操作前后检查了数组内部是否有无用的Entry,如果有就清除掉,算是防止内存泄漏的一个措施。
ThreadLocal概述
为共享变量在每个线程中创建一个副本,每个线程可以访问自己内部的副本变量
private static Integer num = 0;
private static ThreadLocal<Integer> numLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int i = 0;i<threads.length;i++){
threads[i] = new Thread(()->{
int x = numLocal.get().intValue();
x+=5;
numLocal.set(x);
System.out.println(Thread.currentThread().getName()+":"+numLocal.get());
},"Thread-"+i);
}
for (int i = 0;i<threads.length;i++){
threads[i].start();
}
}
重要方法
set
public void set(T value) {
Thread t = Thread.currentThread();
//通过当前线程获取一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果该对象不为空调用set方法,可以猜测类似map接口的set
if (map != null)
map.set(this, value);
//如果该对象是空则调用createMap方法,可以猜测能创建一个ThreadLocalMap对象
else
createMap(t, value);
}
ThreadLocalMap
先来看这个叫做ThreadLocalMap
的内部类:
static class ThreadLocalMap {
/**
* Entry采用弱引用指向一个ThreadLocal对象,这个ThreadLocal始终
* 是这个ThreadLocalMap的键(key)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 这个值代表ThreadLocal关联的值. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//初始容量,容量必须是2的整数次幂
private static final int INITIAL_CAPACITY = 16;
//存放数据的Entry数组,类似于HashMap的Node数组
private Entry[] table;
//Entry的个数
private int size = 0;
//类似于HashMap的threshold,size大于这个值则扩容
private int threshold; // Default to 0
//...
}
从这里可以看出,ThreadLocalMap和HashMap很大的区别是,Entry没有next属性,至少说明不会以链表形式存储。
我们通过getMap()
方法发现,一个Thread对象的内部有一个threadLocals字段,是一个ThreadLocalMap的对象,并且线程创建并不会给这个字段初始化。
那么我们假设第一次在一个线程上调用set方法,此时map==null
,调用createMap()创建一个ThreadLocalMap对象:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到这个方法还顺便把这次要添加的数据添加了进去。
我们返回来看ThreadLocalMap的set方法,看看是否和HashMap类似:
ThreadLocalMap.set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 非常类似HashMap的数组下标计算方法,下文我们将一个数组位置称为slot
int i = key.threadLocalHashCode & (len-1);
// 可以发现,这段代码是用线性探测法解决slot冲突
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// k什么时候为null?因为k是被弱引用的,如果k为null说明中途被回收了,则清理该slot,并把数据放进去
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//先检查i后面的slot是否有被回收的引用,如果有则清理,否则size大于threshold则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap.cleanSomeSlots
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
//检查是否有被回收的引用
if (e != null && e.get() == null) {
n = len;
removed = true;
// 清理该slot
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
ThreadLocalMap.expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 先清理
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 对于还没有被回收的情况,需要做一次 rehash。
//如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置 i,
//则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
ThreadLocalMap.rehash
private void rehash() {
expungeStaleEntries();
// 如果清理完所有的slot,size仍然大于3/4的threshold,立即扩容防止滞后现象
if (size >= threshold - threshold / 4)
resize();
}
threadLocalHashCode
再来看看这个threadLocalHashCode是怎么计算的:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal对象,hash 值就增加一个固定的大小 0x61c88647
。
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//getEntry来查找是否有这个元素
ThreadLocalMap.Entry e = map.getEntry(this);
//如果map中存在该值,返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//否则setInitialValue,并将结果返回
return setInitialValue();
}
getEntry
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//由于采用线性探测法,如果没有找到需要继续向后查找
return getEntryAfterMiss(key, i, e);
}
setInitialValue
private T setInitialValue() {
//从initialValue方法获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//set到map中
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
而initialValue默认返回null,我们可以在创建ThreadLocal对象时重写initialValue方法来指定第一次调用get获取的值。
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//直接调用ThreadLocalMap的remove方法移除
m.remove(this);
}
ThreadLocalMap.remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//在Entry上调用clear
e.clear();
//清理后面的slot
expungeStaleEntry(i);
return;
}
}
}
问题
为什么 ThreadLocalMap 的 Key 是弱引用
如果是强引用,ThreadLocal 将无法被释放内存。
因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是 Java 中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。
听说 ThreadLocalMap 还是会造成内存泄漏?
发生内存泄漏的情况:
- ThreadLocal还被强引用着,但线程已经不需要这份私有数据了,如果不remove掉会一直保留,造成内存泄漏。
- ThreadLocal已经没有强引用了,线程中的ThreadLocalMap中一个Entry的key引用的ThreadLocal对象也被垃圾回收掉了,但该Entry和value值仍会占用空间,不过ThreadLocal会经常检查key为空的Entry并清除,所以该情况可以忽视。
- 在线程池中使用ThreadLocal可能导致内存泄露,因为线程是会一直存在并被复用的,如果不需要这份数据了,需要及时调用remove方法进行清理。
这三种情况总而言之归纳成一句话,不需要这份数据了一定要及时调用remove方法进行清理。
原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/threadlocal%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90%e5%92%8c%e7%9b%b8%e5%85%b3%e7%90%86%e8%a7%a3/