(4.1.10.5)ThreadLocal原理

什么是ThreadLocal?

如果你从字面上来理解,很容易将ThreadLocal理解为『本地线程』,那么你就大错特错了。

首先,ThreadLocal不是线程,更不是本地线程,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

它是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,它们是线程隔离的。接下来,我们通过一个生活案例来开始理解ThreadLocal。

 

一、使用场景

  • 场景1:每个线程需要一个独享的对象,通常是工具类,比如典型的SimpleDateFormat和Random等。

  • 场景2:每个线程内需要保存线程内的全局变量,这样线程在执行多个方法的时候,可以在多个方法中获取这个线程内的全局变量,避免了过度参数传递的问题。

    • Looper MessageQueue

二、原理

  1. 线程类Thread内部持有ThreadLocalMap的成员变量,而ThreadLocalMap是ThreadLocal的内部类
  2. ThreadLocal操作了ThreadLocalMap对象内部的数据,对外暴露的都是ThreadLocal的方法API,隐藏了ThreadLocalMap的具体实现

2.1 ThreadLocalMap数据结构

下图图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系

Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组

  • 查看Thread类,内部维护了两个变量,threadLocals和inheritableThreadLocals
    • 它们的默认值是null,它们的类型是ThreadLocal.ThreadLocalMap
  • 静态内部类ThreadLocalMap维护一个数据结构类型为Entry的数组
    • Entry结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型

2.2 ThreadLocal类set方法

我们并没有直接操作ThreadLocalMap来存取数据,而是通过一个静态的ThreadLocal变量来操作

ublic void set(T value) {
    // 首先获取调用此方法的线程
    Thread t = Thread.currentThread();
    // 将线程传递到getMap方法中来获取ThreadLocalMap,其实就是获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 判断Map是否为空
    if (map != null)
        // 如果Map为不空,说明当前线程内部已经有ThreadLocalMap对象了,那么直接将本ThreadLocal对象作为键,存入的value作为值存储到ThreadLocalMap中
        map.set(this, value);
    else
        // 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
        createMap(t, value);
}

// 获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 首先获取调用此方法的调用线程
    • 将线程传递到getMap方法中来获取ThreadLocalMap,其实就是获取到当前线程的成员变量threadLocals所指向的ThreadLocalMap对象
  • 判断map对象
    • 如果Map为不空,说明当前线程内部已经有ThreadLocalMap对象了,那么直接将本ThreadLocal对象作为键,存入的value作为值存储到ThreadLocalMap中
    • 否则,创建一个ThreadLocalMap对象并将值存入到该对象中,并赋值给当前线程的threadLocals成员变量

最终调用的还是ThreadLocalMap的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;
    // 计算当前ThreadLocal对象作为键在Entry数组中的下标索引
    int i = key.threadLocalHashCode & (len-1);

    // 线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,
    // 判断当前的ThreadLocal对象是否是同一个对象,如果是,那么直接进行值替换,并结束方法,
    // 如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。
    // 这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,
    // 直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。
    // 最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        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对象,如果不为空,则进入到for循环体内,判断当前的ThreadLocal对象是否是同一个对象

如果是,那么直接进行值替换,并结束方法。如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。

这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容

2.2.1 nextIndex方法 开放寻址法

这里有两点需要注意:一是nextIndex方法,二是key失效,这里先解释第一个注意点,第二个注意点涉及到弱引用JVM GC问题,文章最后做出解释。

nextIndex方法的具体代码如下所示:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

其实就是寻找下一个合适位置,找到最后一个后还不合适的话,那么从数组头部重新开始找,且一定可以找到,因为存在扩容阈值,数组必定有冗余的位置存放当前键值对所对应的Entry对象。其实nextIndex方法就是大名鼎鼎的『开放寻址法』的应用。

这一点和HashMap不一样,HashMap存储HashEntry对象发生哈希冲突的时候采用的是链表方式进行存储,而这里是去寻找下一个合适的位置,思想就是『开放寻址法』。

2.3 ThreadLocal类get方法

public T get() {
    // 获取当前线程的ThreadLocalMap对象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 如果map不为空,那么尝试获取Entry数组中以当前ThreadLocal对象为键的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 如果找到,那么直接返回value
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果Map为空或者在Entry数组中没有找到以当前ThreadLocal对象为键的Entry对象,
    // 那么就在这里进行值初始化,值初始化的过程是将null作为值,当前ThreadLocal对象作为键,
    // 存入到当前线程的ThreadLocalMap对象中
    return setInitialValue();
}

// 值初始化过程
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

2.4 ThreadLocal的remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

// 具体的删除指定的值,也是通过遍历寻找,找到就删除,找不到就算了
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) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

三、理解ThreadLocalMap内存泄露问题

ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map中就可能存在key为null但是值不为null的项,所以在使用ThreadLocal的时候要养成及时remove的习惯

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocalMap中的键是ThreadLocal的弱引用

 

  • 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
  • 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在垃圾回收的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值