threadlocal再温习

早时总结过《ThreadLocal解析》《FastThreadLocal解析》

最近看些资料时,又注意到这个类,不尽想再重温下,很多知识点,之前已经总结了,此篇主要有两个问题:

1、弱引用的意义

2、如何防key冲突

弱引用

ThreadLocal底层使用的ThreadLocalMap一直在进化中,早在JDK1.3时代还是使用的普通的HashMap,后来才改写成ThreadLocalMap

这个ThreadLocalMap有两个特殊之处,一是使用了线性探索法,详情见《Hashmap源码解析》;二是key使用了弱引用

1
2
3
4
5
6
7
8
9
10
11
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap{}

从注释可以看出作者为什么使用弱引用,为了处理大对象和长周期对象,在GC时可以主动回收

一直感觉这儿的弱引用设计是个鸡肋

在实际使用中,还是会出现内存泄漏,何必使用弱引用呢?

在ThreadLocal的类注释中

1
2
3
4
5
6
This class provides thread-local variables.  These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).

建议使用static修饰

阿里规范也有此建议:

【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。 说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量, 也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可 以操控这个变量。

在 stackflow上点赞比较多的回答:

Because if it were an instance level field, then it would actually be “Per Thread - Per Instance”, not just a guaranteed “Per Thread.” That isn’t normally the semantic you’re looking for.

Usually it’s holding something like objects that are scoped to a User Conversation, Web Request, etc. You don’t want them also sub-scoped to the instance of the class.
One web request => one Persistence session.
Not one web request => one persistence session per object.

结合实际项目中都是使用线程池的,所以线程基本也是常驻内存的,根据建议使用static修饰,根本没有被回收的机会,虽是弱引用,但一直被强引用

所以何必呢,还让这个点成为一个常备的面试题,同是程序员,非得难为自己人呢!


上面的结论其实还是从大多数人的思维方式思考的,就是提到ThreadLocal都会与线程联系上,在线程的背景里讨论ThreadLocal

但从类注释上看,它是个变量:This class provides thread-local variables,所以还是从变量角度考虑

从变量范围讲,有类变量,局部变量,那ThreadLocal就是线程范围内的变量

根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。

还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。

全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。

ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。

ThreadLocal是跨函数的,但是跨哪些函数呢,由线程来定,更灵活

1
2
3
4
5
6
7
8
9
10
11
public class ThreadLocalDemo
{
public static void main(String[] args)
{
ThreadLocal<Integer> count = new ThreadLocal<Integer>();
//使用count
count.set(10);
// count不再被使用,可以进行内存回收
System.out.println("");
}
}

这个示例中,count要不要被回收

我想,此时JDK的作者也是左右为难,从眼见为实的角度来说,变量不用了就应该进行回收,实现内存的自动回收,这是Java给人最大的特点。

但是,此时不能回收count,因为它与10绑定到了一块,而且只能通过count才能读写10。最后JDK作者耍了一个小聪明,用弱引用包装了count,没有干脆利索的进行内存回收,而是拖拖拉拉的进行回收,反正,最后实现了变量不用就回收的基本原则,与Java的传统思想一脉相承


到此,应该是知道为什么作者要设计为弱引用了,按建议使用ThreadLocal,修饰为static,就不是为了防内存泄漏,而只是为了达到java变量不使用就回收的基本原则,只是在线程范围时,只能曲线救国了

防碰撞

在hash数据结构中,最重要的一点就是如何更好地设计,尽量避免key的冲突

普通的hashmap使用的是拉链法,把长度设置成2的N次方,还有负载因子

ThreadLocalMap使用的线程探索法,作者使用了哪些奇技淫巧

map的长度

1
2
3
4
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap没有设置容量的方法,一般情况下,也不会实例化那么多ThreadLocal实例

必须是2的N次方,这个特性在HashMap中一样,就不解释了,详情见《Hashmap源码解析》

负载因子

在hashmap中,默认负载因子为了0.75,在ThreadLocal中呢?

1
2
3
4
5
6
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

看出负载因子为2/3

但threadlocalmap还有特别的地方,就是弱引用,key可能变成null,所以在get,set都会清理key为null的Entry

1
2
3
4
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();

再看rehash();

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}

rehash方法并不是直接就rehash了,会先清理过期元素,再判断size>= threshold-threshold/4

threshold=len * 2 / 3

threshold-threshold/4=len 2 / 3-(len 2 / 3)/4= len/2

所以最终是在清理过期元素后,元素数量超过len/2时,就会正式扩容两倍

0x61c88647

hash函数的设计是hash类数据结构的一个重点,空间与时间的平衡是关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();

/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;

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

每当创建ThreadLocal实例时这个值都会累加 0x61c88647

0x61c88647转化成十进制:2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值

当容量为2的N次方,并且使用上这个魔法值后,元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列

对于这个数字由来,我也大致搜索了些资料,这已经超出作为一名码农的能力范围,只能反思数学老师讲的:不是数学用不上,而是你没有用上的能力

环形

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

在寻找下一个位置时,虽然threadlocalmap是线性探索,但逻辑上使用环形结构,这个与防冲突关系不大,但也是个知识点,一并罗列下

参考

一针见血 ThreadLocal

Why 0x61c88647?

ThreadLocal源码——黄金分割数的使用

公众号:码农戏码
欢迎关注微信公众号『码农戏码』