美文网首页
线程封闭与ThreadLocal

线程封闭与ThreadLocal

作者: 叠最厚的甲 | 来源:发表于2019-05-02 18:14 被阅读0次

    线程封闭与ThreadLocal

    多线程访问共享可变数据时,涉及到线程间数据同步问题。然而,并不是所有时候都需要共享数据,所以,线程封闭的概念就提出来了。

    通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

    线程封闭的具体体现有:

    • 局部变量
    • ThreadLocal

    局部变量

    局部变量位于执行线程的栈中,其他线程无法访问这个栈。线程封闭是局部变量的固有属性。

    ThreadLocal

    java.lang.ThreadLocal,顾名思义,它可以存放线程本地变量。ThreadLocal让每个线程维护变量的一个副本,各线程通过ThreadLocal去访问该变量时会拿到各自的副本,副本之间相互独立,互不影响,这样竞争条件被彻底消除了。

    使用示例

    下面通过一个例子来验证ThreadLocal的特性。

    public class ThreadLocalTest {
    
        private static final ThreadLocal<String> value = new ThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            value.set("主线程设置的123");
            System.out.println("线程1执行之前,主线程取到的值: " + value.get());
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("线程1取到的值: " + value.get());
                        value.set("线程1设置的值456");
                        System.out.println("重新设置后线程1取到的值: " + value.get());
                        System.out.println("线程1执行结束");
                    } finally {
                        value.remove();
                    }
                }
            }, "线程1");
            thread.start();
            // 等待线程1执行结束
            thread.join();
            System.out.println("线程1执行之后,主线程取到的值: " + value.get());
            value.remove();
        }
    }
    

    这段程序的输出是:

    线程1执行之前,主线程取到的值: 主线程设置的123
    线程1取到的值: null
    重新设置后线程1取到的值: 线程1设置的值456
    线程1执行结束
    线程1执行之后,主线程取到的值: 主线程设置的123
    

    可以看出,不同的线程通过ThreadLocal进行变量的读写时,是互不干扰的。

    原理分析

    ThreadLocal这么神奇,它到底是怎么实现的呢?

    ThreadLocal有3个核心方法:

    • get()
    • set()
    • remove()

    这里主要看get()方法 。

    public T get() {
        // 拿到当前线程对应的ThreadLocalMap对象
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 从map中查询对应的变量副本
        if (map != null) {
            // 以ThreadLocal对象为key,从map中获取ThreadLocalMap.Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果entry不为空,entry的value就是目标变量副本
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 否则,初始化变量副本
        return setInitialValue();
    }
    

    get()方法中可以看出,我们希望得到的变量副本存放在ThreadLocalMap中。而ThreadLocalMap是和线程绑定的:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    Thread类里,有这样一个属性:

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

    ThreadLocalMap的结构如下:

    static class ThreadLocalMap {
    
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        private Entry[] table;
    

    ThreadLocalMap是一个哈希表,它里面存放若干个指向ThreadLocal对象的弱引用,而我们需要的value值就挂靠在这个弱引用上。因此,根据ThreadLocal找到对应的Entry就能拿到目标变量的副本。

    这里使用弱引用的目的是希望在ThreadLocal对象被回收后可以自动回收value对象。

    接下来看get()方法里的第二个分支,setInitialValue()。进入这个分支说明当前线程对应的ThreadLocalMap还未初始化,或者ThreadLocalMap里面还没有初始化ThreadLocal对象对应的Entry

    private T setInitialValue() {
        // 获取初始值(变量副本)
        T value = initialValue();
        // 获取当前线程对应的ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 如果ThreadLocalMap已经初始化,则将ThreadLocal对象和变量副本的映射关系保存在map中
        if (map != null)
            map.set(this, value);
        // 否则,初始化ThreadLocalMap,并保存ThreadLocal对象和变量副本的映射关系
        else
            createMap(t, value);
        // 返回变量副本的值
        return value;
    }
    

    其中,initialValue()的实现是:

    protected T initialValue() {
        return null;
    }
    

    这是一个protected方法,默认返回null值。这意味着,对于一个ThreadLocal对象,线程访问它拿到的默认变量副本是null(这也解释了在前面的示例中线程1一开始拿到的是null值)。我们可以覆盖这个方法,指定一个默认的变量副本,这样可以省去调用get()方法时的一次非空判断。ThreadLocal类里有一个静态内部类SuppliedThreadLocal,它已经帮我们覆盖了默认的initialValue()方法,只需要使用ThreadLocal的静态方法ThreadLocal#withInitial就可以在创建ThreadLocal对象时轻松指定默认值。

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    

    到这里,我们对get()方法的有了大致的了解:获取当前线程的ThreadLocalMap对象,在ThreadLocalMap里以ThreadLocal对象为Key查询EntryEntry对应的value就是我们希望得到的变量副本。如果查找失败,就初始化变量副本(还可能初始化ThreadLocalMap),并存入ThreadLocalMap里,再将变量副本返回给调用者。

    ThreadLocal与使用它的Thread紧密相连:

    • 一个Thread有且仅有一个ThreadLocalMap对象。
    • 一个ThreadLocalMap对象存储多个Entry对象。
    • 一个Entry对象的key的弱引用指向一个ThreadLocal对象。
    • 一个ThreadLocal对象被多个线程所共享。
    • ThreadLocal对象不持有value,value由线程的Entry对象持有。

    了解了get()的实现逻辑,set()remove()方法就不难理解了,这里不再展开。

    注意事项

    ThreadLocal的主要问题是会产生脏数据和内存泄漏。这两个问题通常是在线程池中使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两个特点。

    1. 脏数据

    线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的ThreadLocalMap变量也会被重用。如果在实现的线程的run()方法中不显式的调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个任务不调用set()设置初始值,就有可能get()到重用的线程信息,包括ThreadLocal所关联的线程对象的value值。

    1. 内存泄漏

    通常使用static关键字来修饰ThreadLocal,在此场景下,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的value就不现实了。如果不进行remove()操作,那么ThreadLocal对象持有的value是不会被释放的。

    以上两个问题解决办法很简单,就是在每次用完ThreadLocal时,必须及时调用remove()方法清理

    相关文章

      网友评论

          本文标题:线程封闭与ThreadLocal

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