JAVA并发--ThreadLocal学习之路(一)

JAVA学习网 2017-09-22 14:22:01

      看大神的代码偶然发现代码中的ThreadLocal,一脸不解

      让我们先看下应用代码:只有一个threadlocal实例,一个get方法,一个set方法,一个销毁的方法

private static final ThreadLocal<FootTracerInfo> traceInfo = new ThreadLocal<FootTracerInfo>();

public static FootTracerInfo getTraceInfo() {
    return traceInfo.get();
}

public static void setTraceInfo(FootTracerInfo httpClientInfo) {
    traceInfo.set(httpClientInfo);
}

public static void destroyTraceInfo() {
    traceInfo.remove();
}

    看来这似乎是一个容器,跟踪下get方法,发现这似乎和线程有关,一时激动非常,看来是处理并发环境下使用的


ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

 接下来,看一下google大神的解释吧(节约下时间)

综合看到的文章,总结一下,threadlocal作为一个静态实例可以为每个线程存储一个泛型对象(FootTracerInfo)只对本线程使用,其他线程不可访问。恩,大概是明白ThreadLocal类的用处了。

那么现在我们细细探究下ThreadLocal是如何实现的呢?

我们上面跟踪源码get方法发现,它获取了当前线程,并以此去获取了一个ThreadLocalMap对象,原来这是Thread里面的一个成员变量

    ThreadLocal.ThreadLocalMap threadLocals = null;

 同时我们看看此变量是如果存储的,key是this也就是threadlocal对象实例(traceInfo),value就是我们要存储的FootTracerInfo对象,现在它就只能被自身线程所持有了

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  

也就是说每个线程都有一个独属于它的ThreadLocalMap实例,这样是不是说就解决了变量在线程间不可见

似乎是完事大吉了!!

等等,似乎网友有人说我们可能会用线程池呢,那么之前的线程实例处理完之后出于复用的目的依然存活 ,那么它所持有的值是不是会泄露呢。

 我们知道线程池的主要目的是重用存在的线程,减少对象创建、销毁的开销,这样线程并不会在处理完任务时关闭。 

    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的源码,因为线程并未关闭,所以getMap获取的ThreadLocalMap可能并不为空,而this是静态的ThreadLocal的实例,最终此线程存储的值将会被新的value所覆盖,这时以前的value就不再有强引用指向它,因此将会在下次gc时候回收掉。

      当然,我们看到代码中remove方法,看下源码先,也就是说我们本线程的任务结束时显式调用一下remove方法,将会把当前线程下的map中存储的key,value删除,这样即使使用线程池时,线程未关闭而是重复利用,也不会出现存储的值泄露问题。

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

  现在我们再来探究下ThreadLocalMap中的弱引用问题吧

如下图:

 

我们再来看下ThreadLocalMap的源码,发现这里的key使用了一个对ThreadLocal的弱引用

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

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

  现在我们就探究下这里弱引用的作用,看了许多大神的说法,大概说来:

      1 当key即ThreadLocal对象被置为null时,弱引用不会影响gc对ThreadLocal对象的回收;

      2 当一个线程持有多个ThreadLocal实例时,部分实例可能出现不再有强引用而被gc回收,从而导致map中的key为null,value因强引用而不会被gc回收,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。但是如果thread运行结束,整个线程对象被回收,那么value所引用的对象也就会被垃圾回收。

     什么情况下 ThreadLocal对象会被回收了,典型的就是ThreadLocal对象作为局部对象来使用或者每次使用的时候都new了一个对象。所以一般情况下,ThreadLocal对象都是static的,确保不会被垃圾回收以及任何时候线程都能够访问到这个对象。

     那么再来看看ThreadLocalMap的设计中是如何处理的:

    看看源码,我们在set和get方法中都发现了相似的处理,即擦除key为null位置的entry

   set方法中:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be 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();
}
以及replaceStaleEntry方法中 
// If key not found, put new entry in stale slot 
tab[staleSlot].value = null; 
tab[staleSlot] = new Entry(key, value);

 

 get方法中:

 ThreadLocal k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);


expungeStaleEntry 方法:

           tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;    

除此之外,在源码的rehash方法中,我们看到再判断扩容前先调用了expungeStaleEntries函数,其对map做了一次遍历,对于key为null的entry执行了expungeStaleEntry函数,也即是清除了key为null的entry,那么什么时候调用rehash呢,请看上面set方法的源码

/**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * Expunge all stale entries in the table.
         */
        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);
            }
        }

  

    可见设计者也注意到这个地方可能出现内存泄露,为了防止这种情况发生, 做了如上处理。

 

 

 本文参考了一些大神的文章,现在贴下链接:

1 http://www.cnblogs.com/xzwblog/p/7227509.html

2 http://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/index.html

3 http://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/

4 http://www.cnblogs.com/onlywujun/p/3524675.html

5 http://blog.csdn.net/huachao1001/article/details/51734973

可能还有一些查过没有记录现在也找不到了,抱歉!!

 

 

 

 

 
阅读(760) 评论(0)