主题
《面渣逆袭》Java并发编程 篇 · 第 2/7 章。原版 PDF(下载 / 打印)
ThreadLocal 其实应用场景不是很多,但却是被炸了千百遍的面试老油条,涉及到多线程、数据结构、
JVM ,可问的点比较多,一定要拿下。
10.ThreadLocal是什么?
ThreadLocal ,也就是线程本地变量。如果你创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

创建创建了一个ThreadLoca 变量localVariable ,任何一个线程都能并发访问localVariable 。
java
//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();写入线程可以在任何地方使用localVariable ,写入变量。
localVariable.set("鄙人三某”);读取线程在任何地方读取的都是它写入的变量。
localVariable.get();11.你在工作中用到过ThreadLocal吗?
有用到过的,用来做用户信息上下文的存储。
我们的系统应用是一个典型的MVC 架构,登录后的用户每次访问交叉,都会在请求头中携带一个
token ,在控制层可以根据这个token ,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如r pc 调用、更新用户获取等等,那应该怎么办呢?
一种办法是显式定义用户相关的参数,比如账号、用户名… … 这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?
这时候我们就可以用到ThreadLocal ,在控制层拦截请求把用户信息存入ThreadLocal ,这样我们在任何一个地方,都可以取出ThreadLocal 中存的用户数据。

很多其它场景的cookie 、session 等等数据隔离也都可以通过ThreadLocal 去实现。
我们常用的数据库连接池也用到了ThreadLocal :
数据库连接池的连接交给ThreadLoca 进行管理,保证当前线程的操作都是同一个Connnection 。
12.ThreadLocal怎么实现的呢?
我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程,再获取ThreadLocalMap ,然后把元素存到这个m ap 中。
java
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//讲当前元素存入map
if (map != null)
map.set(this, value);else
createMap(t, value);
}ThreadLocal 实现的秘密都在这个ThreadLocalMap了,可以Thread 类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals。
java
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}ThreadLocalMap 既然被称为M ap ,那么毫无疑问它是< key,value> 型的数据结构。我们都知道m ap 的本质是一个个< key,value> 形式的节点组成的数组,那ThreadLocalMap 的节点是什么样的呢?
static class Entry extends WeakReference<ThreadLocal<?>> {/ThevalueassociatedwiththisThreadLocal.*/
Object value;
//节点类
Entry(ThreadLocal<?> k, Object v) {
//key赋值
super(k);
//value赋值
value = v;
}
}这里的节点,k ey 可以简单低视作ThreadLocal ,value 为代码中放入的值,当然实际上k ey 并不是
ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了 WeakReference(弱引用),再来看一下k ey 怎么赋值的:
java
public WeakReference(T referent) {
super(referent);
}key 的赋值,使用的是We akReference 的赋值。

所以,怎么回答ThreadLocal 原理?要答出这几个点:
Thread 类有一个类型为ThreadLocal.ThreadLocalMap 的实例变量threadLocals ,每个线程都有一个属于自己的ThreadLocalMap 。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value 是ThreadLocal 的泛型值。
每个线程在往ThreadLocal 里设置值的时候,都是往自己的ThreadLocalMap 里存,读也是以某个
ThreadLocal 作为引用,在自己的m ap 里找对应的k ey ,从而实现了线程隔离。
ThreadLocal 本身不存储值,它只是作为一个k ey 来让线程往ThreadLocalMap 里存取值。
13.ThreadLocal 内存泄露是怎么回事?
我们先来分析一下使用ThreadLocal 时的内存,我们都知道,在JVM 中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。
所以呢,栈中存储了ThreadLocal 、Thread 的引用,堆中存储了它们的具体实例。

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。
“ 弱引用:只要垃圾回收机制一运行,不管JVM 的内存空间是否充足,都会回收该对象占用的内存。”那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal (ThreadLocalMap 的K ey )被垃圾回收器回收了,但是ThreadLocalMap 生命周期和Thread 是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的k ey 没了,value 还在,这就会造成了内存泄漏问题。
那怎么解决内存泄漏问题呢?
java
很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal<String> localVariable = new ThreadLocal();
try {
localVariable.set("鄙人三某”);… …
} finally {
localVariable.remove();
}那为什么k ey 还要设计成弱引用?
key 设计成弱引用同样是为了防止内存泄漏。
假如k ey 被设计成强引用,如果ThreadLocalReference 被销毁,此时它指向ThreadLoca 的强引用就没有了,但是此时k ey 还强引用指向ThreadLoca ,就会导致ThreadLocal 不能被回收,这时候就发生了内存泄漏的问题。
14.ThreadLocalMap的结构了解吗?
ThreadLocalMap 虽然被叫做M ap ,其实它是没有实现M ap 交叉的,但是结构还是和HashMap 比较类似的,主要关注的是两个要素:元素数组和散列方法。

元素数组
一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。
java
private Entry[] table;散列方法散列方法就是怎么把对应的k ey 映射到table 数组的相应下标,ThreadLocalMap 用的是哈希取余法,取出k ey 的threadLocalHashCode ,然后和table 数组长度减一& 运算(相当于取余)。
int i = key.threadLocalHashCode & (table.length - 1);这里的threadLocalHashCode 计算有点东西,每创建一个ThreadLocal 对象,它就会新增
0x61c88647,这个值很特殊,它是斐波那契数也叫黄金分割数。hash增量为 这个数字,带来的好处就是hash****分布非常均匀。
java
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}15.ThreadLocalMap怎么解决Hash冲突的?
我们可能都知道HashMap 使用了链表来解决冲突,也就是所谓的链地址法。
ThreadLocalMap 没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式— —开
放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

如上图所示,如果我们插入一个value=27 的数据,通过 hash 计算后应该落入第 4 个槽位中,而槽位 4已经有了 Entry 数据,而且Entry 数据的k ey 和当前不相等。此时就会线性向后查找,一直找到 Entry为 null 的槽位才会停止查找,把元素放到空的槽中。
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的k ey 是否和g et 的k ey 一致,如果不一致,就判断下一个位置。
16.ThreadLocalMap扩容机制了解吗?
在ThreadLocalMap.set() 方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散
列数组中Entry 的数量已经达到了列表的扩容阈值(len*2/3) ,就开始执行rehash() 逻辑:
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
再着看rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断size >= threshold -threshold/4也就是size>=threshold*3/4来决定是否需要扩容。
java
private void rehash() {
//清理过期Entry
expungeStaleEntries();
//扩容
if (size >= threshold - threshold / 4)
resize();
}
//清理过期Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}接着看看具体的resize()方法,扩容后的newTab的大小为老数组的两倍,然后遍历老的table 数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有
的entry 数据都已经放入到newTab 中了,然后table引用指向newTab
具体代码:

17.父子线程怎么共享数据?
父线程能用ThreadLocal 来给子线程传值吗?毫无疑问,不能。那该怎么办?
这时候可以用到另外一个类— —InheritableThreadLocal。
使用起来很简单,在主线程的InheritableThreadLocal 实例设置值,在子线程中就可以拿到了。
java
public class InheritableThreadLocalTest {
public static void main(String[] args) {
final ThreadLocal threadLocal = new InheritableThreadLocal();
// 主线程
threadLocal.set("不擅技术");
//子线程
Thread t = new Thread() {@ Override
java
public void run() {
super.run();
System.out.println("鄙人三某 ," + threadLocal.get());
}
};
t.start();
}
}那原理是什么呢?
原理很简单,在Thread 类里还有另外一个变量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;在Thread.init 的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);