深入理解ThreadLocal

以前看过ThreadLocal,今天朋友问我这个问题,突然自己记不清了。又考虑到相关书籍都是对ThreadLocal一笔带过,网上博客讲解的不太全面,于是决定写下这篇文章做一个总结。

本文主要介绍 set、get方法和hashcode。

 

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal包含ThreadLocalMap的静态内部类,该内部类包含一个Entry[]哈希表,默认长度16位、加载因子2/3、扩容为原长度2倍、哈希冲突采用开放定址法。

Thread类包含ThreadLocal.ThreadLocalMap的引用,ThreadLocal工作原理是将ThreadLocal对象作为key、任意Object作为value存入到ThreadLocalMap的Entry[]数组里,根据ThreadLocal的threadLocalHashCode与哈希表length-1的与运算计算出它在桶的位置,要注意的是key是一个弱引用且只是逻辑存在于哈希表中。

为什么 ThreadLocalMap 的 key 是弱引用?

我们知道 ThreadLocalMap 中的 key 是弱引用,而 value 是强引用才会导致内存泄露的问题,至于为什么要这样设计,这样分为两种情况来讨论:

  • key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。
  • key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。

比较以上两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。

 

图片来源于网络,侵删。

使用示例

public class ThreadLocalDemo  {

    private static ThreadLocal<String> local=new ThreadLocal<String>(){

        @Override

        protected String initialValue() {

            return "xinzhuoyue";

        }

    };

    public static void main(String[] args) {

        local.set("hahahahah");

        System.out.println(local.get());

        new Thread(){

            @Override

            public void run() {

                local.set("very gooood");

                System.out.println(local.get());

               local.remove();

            }

        }.start();

        new Thread(){

            @Override

            public void run() {

                //local.set("666");

                System.out.println(local.get());

            }

        }.start();

        local.remove();

    }

}

输出结果:

hahahahah

verygooood

xinzhuoyue

一、Set

1.    线程都拥有一个ThreadLocal.ThreadLocalMap threadLocals 的引用。

    ThreadLocal.ThreadLocalMap threadLocals = null;

2.   创建一个ThreadLoal类,当线程调用threadLoad的set(value)方法时,该方法获取了本线程的引用,调用getMap(t)方法,获得本线程的ThreadLocal.ThreadLocalMap引用map。

public void set(T value) {

    Thread t = Thread.currentThread();

    ThreadLocalMap map =getMap(t);

    if (map != null)

        map.set(this, value);

    else

        createMap(t, value);

}

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

3.    若map不为null。调用map.set,根据本ThreadLoal对象的threadLocalHashCode&(哈希表长度-1)算出在Entry数组中的位置,将Entry放进桶中。若hash冲突采用开放定址法解决。

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

4.    若map为null,则根据本ThreadLoal对象的threadLocalHashCode与Entry数组的长度与运算,计算出在Entry数组中的位置并放入创建的Entry对象

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

5.   可以看到table[i] = new Entry(firstKey,firstValue);这一步创建了Entry对象——ThreadLoad的内部类ThreadLocalMap的内部类Entry。可以看到Entry继承了WeakReference类,并将ThreadLocal<?>  key设为弱引用(只能生存到下次垃圾回收之前)。将Object v引用赋给自己的成员Object value。在这里可以看出Entry中并没有存入key,所以key实际上不存在于哈希表中。

static class ThreadLocalMap {

    /**

     * Theentries in this hash map extend WeakReference, using

     * its mainref field as the key (which is always a

     *ThreadLocal object).  Note that null keys(i.e. entry.get()

     * == null)mean that the key is no longer referenced, so the

     * entrycan be expunged from table.  Such entriesare referred to

     * as"stale entries" in the code that follows.

     */

    static class Entry extends WeakReference<ThreadLocal<?>> {

        /** The value associated with this ThreadLocal. */

        Object value;

       Entry(ThreadLocal<?> k, Object v) {

            super(k);

            value = v;

        }

    }

…………………………………………

}

6.   继续看ThreadLocalMap的构造函数。可看到 setThreshold(INITIAL_CAPACITY);

可知其加载因子时2/3, 每次Entry数组长度扩容为原来的两倍

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

二、Get

1.  Get方法也会首先获取当前线程的ThreadLocalMap。
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();
}

2.  如果map不为null,将ThreadLocalthreadLocalHashCodetable.length - 1相与,获取到Entry数组中的i角标元素。若是本ThreadLocal则返回,若不是说明发生了哈希冲突使用了开放定址,所以需要继续向下遍历查找。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

3.  继续往下看第一步中的get()方法。若第二部找到了Entry,则获取它的成员变量Objectvalue并返回。

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

4.  若 本线程还没有ThreadLocalMap引用为null或不能根据本ThreadLocal对象的threadLocalHashCode找到Entry,则返回setInitialValue(),可以看到setInitialValue()方法首先调用的initialValue()默认值是一个null。

然后获取本线程引用的ThreadLocalMap对象,并调用set方法或创建一个ThreadLocalMap(上面第一部分已经说过)。并返回value(null)。

所以想要自己设初始值,需要重写initialValue()
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;
}
protected T initialValue() {
    return null;
}

三、hashcode

ThreadLocal的属性threadLocalHashCode代表本ThreadLocal对象的哈希值,可以看到该属性被final修饰不可变。其初始化调用的nextHashCode()与nextHashCode是static修饰的,所以每创建一个ThreadLocal对象,可以保证nextHashCode被更新最新的值,并且AtomicInteger保证了nextHashCode的原子性。

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
    new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
所以可知,一个线程可以利用多个Threadlocal对象存入多个键值对。

四、内存泄漏

在第一部分的第5步可以看到,Entry类继承了WeakReference类,将ThreadLocal的引用设为弱引用。

Java引用分为强引用、软引用、弱引用、虚引用。强度递减。

a.      强引用就是Object obj=new Object()之内的

b.      软引用内存发生溢出异常前,把这些对象列入回收范围二次回收,若还是内存不足则拋内存溢出异常。SortReference类实现。

c.      弱引用只能生存到下次垃圾回收之前。WeakReference类实现

d.      虚引用无法通过其得到一个对象实例,为对象设置虚函数唯一目的就是能在这个对象被回收时收到一个系统通知。PhantomReference类实现

所以存在一个阶段,当用户的ThreadLocal对象被回收,而此时使用过该线程还没执行结束,线程还保持有ThreadLocalMap的引用,map中的Entry元素再失去了key后无法被引用到,在线程运行结束之前的这段时间就发生了内存泄露。

当然,JDK设计的时候肯定考虑到这种情况,我们继续往下看。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
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;
}

红色字体部分就是擦除key为null的Entry。

但即使JDK光有这种方案还是不够的,只有调用了getEntry方法才会删除那些没用的Entry。所以我们使用完ThreadLocal时最好手动remove无用的Entry防止内存泄漏,在定义ThreadLocal时建议将它定义成private static延长生命周期。

 参考文章:https://www.zhihu.com/question/23089780

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值