Skip to content

《面渣逆袭》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);
本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传