有关ThreadLocal的总结

定义

java api中是这样定义的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻译过来:

这个类提供了线程本地变量(thread-local)。线程本地变量(thread-local)区别于线程普通的变量副本,通过线程本地变量(thread-local)提供的get和set方法,可以为每个线程提供其独立的变量副本。通常在类中声明threadlocal为static。

理解的意思就是 Threadlocal就是为每一个线程都维护了自己独有的变量拷贝,每个线程独有,这样竞态条件就被彻底的消除了。

那就不需要考虑并发同步的情况,这样是用空间换时间的方法,来解决并发问题。

使用

先写个最简单朴实的代码来打个断点感受下

public class Test4 {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        threadLocal.set(1);

        System.out.println("----");
    }
}

在java的main线程了,新建了一个threadlocal的变量,并且塞入值为1,断点就打在System.out.println这行

可以看到,main线程中已经有这个threadlocal变量的值了。(注意这里有一个问题,main线程本身就已经有三个threadlocal变量了,这个后面弄一篇新的文章来解释这个)

请注意,我执行的那句话Thread.currentThread().threadlocals.table,这个threadlocals变量是在Thread类中声明的

扫描二维码关注公众号,回复: 2558526 查看本文章
 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

所以,大体上,通过threadlocal.set(1),可以知道,最终这个1是保存在当前线程的ThreadLocalMap中。

接下来,就开始看这个threadlocal的set方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread(); 
        // 获取当前线程中的threadlocalmap
        ThreadLocalMap map = getMap(t);
        //有就塞值,没有就创建一个新的  
        if (map != null)  
            map.set(this, value);
        else
            createMap(t, value);
    }

代码已经加了相关的逻辑注释,先是获取当前线程,然后用当前线程作为参数,去获取当前线程中的ThreadLocal.ThreadLocalMap,剩下的就是对map进行设置值。

重点就是在这里了:

1. ThreadLocalMap是ThreadLocal的内部类。为什么是内部类,而不是一个全局变量?因为threadlocal是本意是避免并发的,如果设置成一个全局变量,那么当前线程有多个threadlocal的时候,map的读写就会出现竞态

 2. map.set(this, value),key是当前的threadlocal变量

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

key.threadLocalHashCode & (len-1) 这句代码要重点关注了,很有意思的地方。

这里就要讲下hash碰撞的解决了。

1. 当前线程只new了一个threadlocal对象

2. 当前线程new了多个threadlocal对象

先看下threadlocal的构造方法,是空的,并没有做任何事情,但是这个类却声明了一个static类型的nextHashCode变量,这个变量在类创建的时候会初始化,初始化的代码在nextHashCode()中,每次累加HASH_INCREMENT 即0x61c88647。同时将这个的值赋给threadLocalHashCode,而len这个变量呢,就是ThreadLocalMap.ThreadLocalMap中维护的Entry数组的大小,这个数组的大小原始值为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方。所以,key.threadLocalHashCode & (len-1) 这个的与运算,相当于key.threadLocalHashCode % len 取模运算,这样就能让hash的结果在2的n次方内尽可能均匀分布,减少冲突的概率。

ThreadLocal的内存泄露

ThreadLocal的内部类ThreadLocalMap中维护着Entry的数组,而这个对象Entry被声明为弱引用。

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

看上去,正常是不会出现内存泄漏的。但是注意这里的这个弱引用实际上是对key的弱引用。这里盗个其他作者的图

当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有thead退出以后,value的强引用链条才会断掉

为了防止这种问题,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的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); //清理key为null 的value值
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这样难道就真的没有内存泄露的可能了么,假如在程序中,使用了threadlocal,并且使用之后直接将threadlocal直接设置为null(这也是为什么java推荐用static来初始化threadlocal原因),那么这个value引用的对象,短时间就不会被回收。再遇见一种极端情况,这个线程迟迟不销毁,尤其是遇见这个线程属于线程池的,始终都被频繁复用。最终就会导致内存泄露了,所以写代码的时候一定要注意这点。

所以,使用threadlocal 的时候,用完都要remove下,这个也是编程规范

工作中使用的场景

spring request中从接收请求,处理请求,到返回请求,只要线程不销毁,就可以在线程中的任何地方,调用这个参数。所以,通常都是结合项目会话,来处理会话的相关内容,利用threadlocal设置会话的上下文。

猜你喜欢

转载自blog.csdn.net/u010372981/article/details/81354926