多线程并发JVM

ThreadLocal的作用和原理

2021-06-23  本文已影响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;
            }
        }
上一篇下一篇

猜你喜欢

热点阅读