JavaThreadLocal

Posted 绝世好阿狸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaThreadLocal相关的知识,希望对你有一定的参考价值。

ThreadLocal

作用

线程级别变量隔离,减少多线程访问变量时加锁带来的性能损耗。

缺点

使用不当可能导致oom。

原理

弱引用是精髓。

get方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal解决并发的思路就是在每一个线程里都存放一份变量的值,互不干扰。

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到,Thread对象里有一个ThreadLocalMap类型的变量,这本质是一个map,key是ThreadLocal实例,value是对应的值。可以看到get方法就是从当前线程对象中的map里取到对应的k-v,返回。

set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

同理,set方法也是类似,将k-v塞到当前线程对象的map结构里。

 

关键看下这个map结构

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

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

map结构的Entry定义如上,可以看到整个Entry就是一个WeakReference类型。

private Entry[] table;

ThreadLocalMap的核心成员变量是Entry类型的数组。那么,这里的map结构其实是用指针退避法来解决hash冲突。正常的map结构是拉链法,冲突的key构成一个链表。而采用指针退避法的map,遇到hash冲突时,会找下一个slot。

我们可以从该map的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();

        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();
}

首先计算出key的哈希值,然后计算出slot的下标i,从i开始找第一个为空的slot,也就是for循环结束后的语句。然后new出一个Entry实例,赋值给tab[i]。这是最常规的情况。别忘了,Entry类型是弱引用,所以,如果没有任何强引用指向,该对象可能被回收。这样就会命中for循环里的第二个if,需要将当前entry替换为新的值,也就是replaceStableEntry方法。这里不做深入。

同时,在方法的结尾处,也会调用cleanSomeSlots方法检测map里的无效弱引用数据,进行清理和rehash:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

遍历一遍slot,对无效弱引用调用expungeStaleEntry方法清理和rehash:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

实际上,每一次的get set remove方法也会触发ThreadLocalMap对无效弱引用的清理和rehash操作。

 

弱引用的意义

如果是用强引用,只要线程存在,ThreadLocal对象就会被引用到,这样比较消耗内存。举个例子,web容器比如tomcat,我们可以用ThreadLocal变量存放请求上下文信息。一次请求中可能存了一个很冷门的数据,只在1%的请求中会被使用。当这一次请求用完后,接下来的99个请求都不会用,但是由于线程还是那一个,所以这个对象无法被回收。如果换成弱引用,这一次请求结束后,就没有强引用指向这个对象了,gc时就会将这个ThreadLocal变量指向的对象回收(回收之后,map里的entry就指向了null,会在get set remove方法执行时清理掉,同时rehash),节约了内存。

 

参考:https://juejin.cn/post/6844904046373896205

 

 

以上是关于JavaThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

JavaThreadLocal

JavaThreadLocal

JAVAThreadLocal源码分析

JavaThreadLocal

javaThreadLocal线程变量的实现原理和使用场景

微信小程序代码片段