美文网首页多线程并发JVM
ThreadLocal的作用和原理

ThreadLocal的作用和原理

作者: 肥兔子爱豆畜子 | 来源:发表于2021-06-23 11:13 被阅读0次

    ThreadLocal可以称为线程本地变量或线程本地存储,跟方法内作用域的变量一样,都是本线程私有的。可以用来在一个线程调用多个方法的过程中、用来传递参数,省去通过方法入参传递的麻烦。slfj的MDC,多数据源,以及弱引用WeakReference等等场景中都可以看到ThreadLocal的应用。

    线程本地存储:在线程生命周期内作为上下文存储共享对象

    这里的上下文指的是线程存活期间内,调用多个方法,各个方法之间共享的“上下文空间”。

    我们知道,每个线程对应着它的线程栈,线程栈由栈帧组成,用这套数据结构来跟踪线程的方法调用。
    每个栈帧里边存放着一个方法内的局部变量,进入一个方法则压入一个栈帧,从一个方法返回则弹出一个栈帧。
    考虑一个问题:如果想在一个thread生命周期内,在多个栈帧或者说多个方法之间共享对象呢?
    用局部变量显然不行,其作用域只在方法里或者栈帧内,每个栈帧维护自己的局部变量表,另一个栈帧不认识。thread里边弄个静态变量当然可以,但是这是类级别的、就对别的thread实例可见,要考虑并发问题了。
    想来想去,在Thread类的内部的成员变量中搞个Map来存放这些值是个不错的主意:作用域是每个thread实例,能够被线程生命周期内各个方法调用所共享。我想这就是ThradLocalMap和ThreadLocal的由来。

    ThreadLocal的使用方法

    先看例子程序:

    使用ThreadLocal在线程的多个方法调用之间共享参数

    public class WorkerThread implements Runnable{
        public static ThreadLocal<Map> paramA = new ThreadLocal<>();
        private CountDownLatch latch;
        
        public WorkerThread(CountDownLatch latch) {
            this.latch = latch;
        }
        
        @Override
        public void run() {
            ThreadLocalTest tlt = new ThreadLocalTest();
            tlt.initParamA();
            tlt.useParamA();
            latch.countDown();
        }
    
    }
    
    public class ThreadLocalTest {
        private static Logger logger = LoggerFactory.getLogger(ThreadLocalTest.class);
        private static int N = 100;
        public static void main(String[] args) {
            CountDownLatch latch = new CountDownLatch(N);
            for(int i=0; i<N; i++) {
                new Thread(new WorkerThread(latch)).start();
            }
            try {
                latch.await(); //等所有WorkerThread线程都执行完
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public void initParamA() {
            HashMap<String,String> map = new HashMap<>();
            map.put("name", Thread.currentThread().getName());
            WorkerThread.paramA.set(map);
        }
        
        public void useParamA() {
            String name = (String)WorkerThread.paramA.get().get("name");
            String threadName = Thread.currentThread().getName();
            logger.info("当前线程名{},通过 ThreadLocal传递的线程名{}", threadName, name);
            if(!threadName.equals(name))
                logger.error("出现并发问题");
        }
    }
    

    这个程序的意图是这样的:worker线程会去调用initParamA和useParamA两个方法,使用ThreadLocal在两者之间传递一个参数,这里传递的是一个Map,里边放了当前worker线程的线程名。最后会通过比较Thread.currentThread().getName()与ThreadLocal里的线程名是否相等来证明ThreadLocal的线程私有性。
    运行结果是不会打印出"出现并发问题"。

    源代码分析

    ThreadLocal

    我们的ThreadLocal是找了一个类声明了一个静态成员变量

    public static ThreadLocal<Map> paramA = new ThreadLocal<>();
    

    然后分别是在不同的方法里调用了set()和get()方法来放和取我们的参数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);
        }
    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的set和get是把自己ThreadLocal对象作为key,和我们的参数作为value,组成k-v对,放在当前Thread的ThreadLocalMap这个Map里的。每个线程都有自己的ThreadLocalMap,这是定义在Thread.java里的。

    ThreadLocalMap

    ThreadLocal<Map> paramA = new ThreadLocal<>();
    

    实例化了一个ThreadLocal,paramA,当调用paramA.set(Map)时,这个Map最终存放在当前线程的ThreadLocalMap里。ThreadLocalMap是每个线程Thread实例内部都有的一个存储结构,里边实际上是个Entry数组,每个Entry由ThreadLocal和Map这样一个k-v对来实例化。

    也就是说ThreadLocal和Map这样一个k-v在ThreadLocalMap中存放时,是封装成Entry存放的,而Entry是存放在ThreadLocalMap的private Entry[] table这个数组中的,存放时先根据key的hash找到对应的Entry数组下标,然后找到对应的Entry,如果Entry的key等于当前这个ThreadLocal,那么就用我们的Map替换Entry的value,否则直接new Entry然后放到Entry数组的下标位置。

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

    如何保证通过ThreadLocal在线程生命周期内共享的对象的正确回收

    通过ThreadLocal和ThreadLocalMap机制在线程生命周期内共享对象,引出了另一个问题:我们用来在两个栈帧之间共享的这个对象,在这两个栈帧弹出之后,从使用者角度来说理论上这个变量就没用了,应该被gc了。且这时候这两个栈帧弹出,那两个方法中的局部变量与这个对象之间的强引用关系也不存在了,这对象可以回收了吗?
    仅仅是这样显然还不行,如果这个时候线程还没执行完,那这个线程的成员变量ThreadLocalMap也还在堆里边呢,共享的对象也就是我们的Map就以Entry的形式存放在ThreadLocalMap里,也就是说ThreadLocalMap的Entry与我们的共享对象存在着引用关系。那怎么才能正确的释放我们的Map对象呢?

    看一下Entry的定义:

    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
    • 首先Entry继承了WeakReference,其中key也就是ThreadLocal对象本身用WeakReference包装起来了,见代码中的super(k)。这样一来如果是在方法中定义了ThreadLocal local = new ThreadLocal(),那么当方法返回,栈帧弹出,ThreadLocal对象失去了引用,而其与Entry之间的引用又是弱引用,所以它是可以被gc顺利回收的。

    • 其次,我们需要注意,Entry构造方法里边只是把key作为弱引用包装起来了,而value只是个一般的成员变量类型。这样的话,当发生gc的时候,由于Entry的key也就是ThreadLocal是被WeakReference包装起来的,所以它会被垃圾回收。但如果Entry没有失去gc root引用,那这个value不会主动释放的。也就是线程没有执行完,Entry不会失去引用,我们的共享对象也不会失去引用,这个引用还是个强引用,所以我们的对象不会被垃圾回收而自动释放!

      这一点要特别注意,如果我们的线程生命周期要很长,比如是在一个线程池里被复用的线程,那么其对应的ThreadLocalMap还有里边Entry都会存活不会回收。需要当我们确认通过ThreadLocal存储的对象不再使用的时候,最好手工调用remove()方法来清理Entry。

    • 最后,一旦线程生命周期结束,线程栈销毁,线程方法内局部变量和ThreadLocalMap里的Entry对ThreadLocal存储对象的引用就都消失了,这个对象将会被垃圾回收。

    相关文章

      网友评论

        本文标题:ThreadLocal的作用和原理

        本文链接:https://www.haomeiwen.com/subject/yztyyltx.html