ThreadLocal源码分析

1,092 阅读5分钟

一、概述

ThreadLocal,即线程局部变量。主要用于线程间数据隔离。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。 ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

二、原理

ThreadLocal如何实现线程独立访问ThreadLocal关联的变量呢?

这里主要有两种方式:

  1. 在ThreadLocal中维护一个map,map的key是线程,value是关联的变量。但这种方式不太优雅(JDK1.5之前采用的这种方式),比如说可能会导致线程很大,而且当线程销毁时,还需要在map中将其删除,在多线程情形下,会增加维护难度和时间成本。
  2. 每个Thread维护一个ThreadLoaclMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样做有很多好处,比如不用加锁来保证读写安全,而且当线程销毁时,与其关联的ThreadLocalMap也自然消亡。

image.png

ThreadLocalMap的说明

ThreadLocal没有直接使用HashMap而是自己重新开发了一个map,最主要的作用是让他的key为虚引用类型,这样当ThreadLocal对象销毁时,多个持有其引用的线程不会影响它的回收。 ThreadLocalMap是一个很像HashMap的一个数据结构,但他并没有实现Map接口,而且它的Entry是继承WeakReference的,也没有next指针,所以不存在链表了。对于hash冲突,而是采用的开放地址法来进行解决 ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次rehash操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的3/4才会进行真正意义上的扩容

// ThreadLocalMap没有继承Map接口
static class ThreadLocalMap{
    // Entry被声明为弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

// 调用rehas方法
private void rehash() {
    // 清除过期数据
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 清除过期数据后数据仍然大于等于总容量的3/4,则扩容
    if (size >= threshold - threshold / 4)
        resize();
}

get/set/初始化

当调用get或set方法时,首先会去检查线程的ThreadLocalMap是否被初始化,如果没有初始化,则会进行初始化操作,否则根据计算出来的key找到对应下标,如果对应下标是我们要找的元素,则返回,否则会向后查找,直到碰到slot为null或者找到为止。在这过程中同时会清理过期的K-V对,set同理。具体可以参看源码。

// get方法
public T get() {
    // 获取当前线程类
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判断是否初始化,如果没有则开始初始化
    if (map != null) {
        // 如果当前下标是目标元素,则返回,否则调用getEntryAfterMiss方法查找
        // getEntryAfterMiss方法会继续查找并清理过期数据
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
                T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}


// 初始化
private T setInitialValue() {
    // 默认返回null,可以重新此方法决定每个线程在初始化map时获取的值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    ....
    return value;
}

// set方法
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {
        ThreadLocal<?> k = e.get();
        // 如果key相同,直接替换就行
        if (k == key) {
            e.value = value;
            return;
        }
        // 过期数据,需要清理
        // replaceStaleEntry函数逻辑:向后查找,直到遇到目标元素更新数据或桶位为空
        // 插入值并清除过期数据
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 最后插入我们的目标元素
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

三、内存泄漏

强引用内存泄漏

如果Entry的key为强引用,则会导致ThreadLocal实例在被创建它的线程销毁时,而无法被回收,从而导致严重的内存泄漏问题,因此Eetry的key被声明为弱引用来避免这种问题

弱引用内存泄露

我们知道每一个线程都存在一个ThreadLocalMap,Map中的key为一个ThreadLocal实例。而且key到ThreadLocal实例的引用为虚引用,也就是说当ThreadLocal置为null时,没有任何强引用指向ThreadLocal实例,所以ThreadLocal实例会被GC回收。但是value却不能被回收,因为存在一条从当前线程连接过来的强引用(Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value) 针对上面的内存泄露问题,ThreadLocal在get和set时都会检测并清除key为null的Entry,从而尽可能的避免内存泄露 image.png

使用建议

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据
  • 将ThreadLocal声明为private static,使它的生命周期与线程保持一致

四、总结

本文简单介绍了ThreadLocal相关的原理并分析了部分主要源码,如有错误,还望不吝指正,感谢~