一、ThreadLocal 线程私有的。

为什么说ThreadLocal是线程私有的?  上源码 ThreadLocal.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);
    }

从源码得知。我们在往 ThreadLocal 中存数据时。 它首先 获取到当前线程。并从当前线程中拿到 ThreadLocalMap 。再往里存放数据。

接下来我们再来看ThreadLocalMap 中  key ,value 究竟又存放的是什么。显然 ThreadLocalMap 中 key ->ThreadLocal  ;value ->我们set 进去的 value。

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

由源码,我们很自然就能得出结论。 既然调用 ThreadLocal .set()  方法时,每次都是获取当前线程,再完成数据存储动作的。那么,它自然 具备了线程隔离性。由某一个线程所持有。所以ThreadLocal是线程私有的。

接下来,我们用代码来证明

/**
 *  首先 我先初始化一个 threadLocal 。、
 *  再主线程main 中 新启一个线程 thread,并在该线程中完成 set操作。
 *  由此 当前应用存在两个线程 一个是 main ,一个是 thread
 *  由于threadLocal 是线程私有的。所以 主线程 main 获取的值为 null
 */
public class Test {

    public static void main(String[] args) {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        Thread thread = new Thread(()->{
            System.out.println(String.format("往 线程%s 中 存入 1 ",Thread.currentThread().getName()));
            threadLocal.set(1);
            try{
                Thread.sleep(1000);
                System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
            }catch(Exception ex){
                ex.printStackTrace();
            }

        });
        thread.start();

        try{
            // 让 主线程睡一会儿。确保 cpu 已执行 thread 中的 run()方法
            Thread.sleep(500);
        }catch(Exception ex){
            ex.printStackTrace();
        }

        System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
    }
}

简谈ThreadLocal-LMLPHP

验证完毕。让我们继续来看下源码。

1.1 首先,我们来看一下 Thread 类。java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals

  /*java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals */


/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

从上图我们可以看出ThreadLocalMap 是 ThreadLocal 中的  静态内部类。

1.2 接着我们来看下 ThreadLocal 中的源码。

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref 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
         * entry can be expunged from table.  Such entries are 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;
            }
        }

看过源码后我们发现  ThreadLocalMap  中  也包含一个 静态内部类 Entry 继承自 WeakReference<ThreadLocal<?>> 同时拥有 一个ThreadLocal 的引用。和 value。

那么问题来了。此处为什么要用 弱引用?

首先。我们再来复习一下,弱引用的特点。  当一个对象没有被强引用存在时。 弱引用 将被jvm忽视。直接gc掉。想一下,如果不是 弱引用。而是强引用,会有什么问题。假使 Entry 未继承弱引用,则 entry 对 ThreadLocal  强引用。 则 当  ThreadLocal 被复制为null时 意味着,ThreadLocal 已经没有用了。 但 entry 对 ThreadLocal  的 强引用。导致 ThreadLocal  没有办法被回收。 会造成内存泄漏。 所以 这里被继承自 弱引用。

验证来了。上代码,看看 ThreadLocal 被置为空后。 是否被gc了。

/**
 * 首先 我创建了两个 ThreadLocal 对象 分别是 threadLocal,threadLocal_2
 * 分别往 主线程 main 中 存放数字 1,2
 * 然后将 threadLocal 置为空。 然后再 手动 gc 。
 * 分别再gc 前 和 gc 后 查看 当前 线程中的 ThreadLocalMap。
 * 由于获取 ThreadLocalMap 访问级别为default  所以我这里将用 debug的方式进行查看。
 */
public class Test1 {

    public static void main(String[] args) {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        ThreadLocal<Integer> threadLocal_2 = new ThreadLocal<>();

        threadLocal.set(1);

        threadLocal_2.set(2);

        System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));

        threadLocal_2.get();

        threadLocal = null;

        System.gc();
        threadLocal_2.get();



    }
}

未gc前。debug 看到的 情况。

简谈ThreadLocal-LMLPHP

gc 后。 debug 看到的情况。

简谈ThreadLocal-LMLPHP

终上所诉。验证了 弱引用的实际效果。 和 为什么不用强引用 的原因。

这里,我们发现了。 弱引用后, ThreadLocalMap 中对应的 entry 的 referent (指向 threadLocal的引用)确实被gc了。 

继续看源码。 referent 是哪里来的。 entry 继承自 WeakReference<ThreadLocal<?>> 。而 WeakReference 又继承自 抽象类 Reference<T>。

private T referent;         /* Treated specially by GC */

    volatile ReferenceQueue<? super T> queue;

    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    volatile Reference next;

好,了解到这里。 我们不难发现。 虽然被gc了。但是 还是存在一个问题,ThreadLocalMap  -》entry -》key -》 referent (threadLocal)已然为null了。但是却仍然存在ThreadLocalMap中,占用的部分的内存。 应用不可能通过某个引用再次拿到 entry 中的value了。 那不就是内存泄漏了吗?

好问题! 这里编写人员,也想到了该问题。所以,他们的处理逻辑是:再调用 set,remove 方法时,调用方法 expungeStaleEntry 将 键为null 的对象remove掉。

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


// 将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;


    // Rehash until we encounter null
    Entry e;
    int i;

   // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
// 如果entry的key为null,则清除掉该entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {   // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
                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)      // 对该entry,重新进行hash并解决冲突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
 // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
    return i;
}

到这里,这样处理,就万事大吉了吗? 远远没有这么简单。 新问题又来了。 假使 set完后,就再也没有调用 set 和 remove 方法。那 不还是内存泄漏了吗?

所以再使用 ThreadLocal 时,要养成一个好习惯,ThreadLocal 再没有用时,就将 ThreadLocal 置为null。以免出现内存泄漏。

最后,我们来分析一个问题。ThreadLocal  是线程私有的。如果是多线程中(多线程中的线程是可复用的)使用了 ThreadLocal 会有什么问题?

答案也很简单。如果没有及时清理 ThreadLocal 除内存泄露外,还可能引发数据问题。 话不多说, 上代码。

/**
 * 先后创建6个任务,前3个线程写数据,后3个线程读取数据。
 *
 * 发现threadLocal 里的 值被取出了。
 *
 * 假使有个业务场景是 往当前线程 存放 用户名(采用ThreadLocal来存储) 。
 * 先进行非空判断,再进行 存储。 结果,悲剧了。 上个线程的 ThreadLocal 并没有清理。导致 不为空。
 * 结果上线文中存储的用户名就乱套了。可能就张冠李戴了
 */
public class Test2 {

    public static void main(String[] args) {

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        ThreadLocal<String> threadLocal = new ThreadLocal<String>();

        for(int i=0;i<3;i++){
            fixedThreadPool.execute(()->{
                try {
                    threadLocal.set("ckr");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }


        try{
            Thread.sleep(2000);
        }catch(Exception ex){

        }

        for(int i=0;i<3;i++){
            fixedThreadPool.execute(()->{
                try {
                    System.out.println(threadLocal.get());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }


        try{
            Thread.sleep(2000);
        }catch(Exception ex){

        }

        fixedThreadPool.shutdown();
    }
}

以上。关于ThreadLocal的一些问题,我们都了解了,再次,特别强调,ThreadLocal 有风险。需要谨慎使用

关于 java 中的四种引用强,软,弱,虚。 请查看上篇博客: https://my.oschina.net/u/4141142/blog/4517998

07-05 17:06