zl程序教程

您现在的位置是:首页 >  其他

当前栏目

ThreadLocal 详解

2023-04-18 14:55:31 时间

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

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

我们知道,每个线程对应着它的线程栈,线程栈由栈帧组成,用这套数据结构来跟踪线程的方法调用。

每个栈帧里边存放着一个方法内的局部变量,进入一个方法则压入一个栈帧,从一个方法返回则弹出一个栈帧。

考虑一个问题:如果想在一个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存储对象的引用就都消失了,这个对象将会被垃圾回收。