多线程知识梳理(9) - ThreadLocal

作者: 泽毛 | 来源:发表于2018-01-17 22:57 被阅读129次

    一、基本概念

    1.1 ThreadLocal 的用途

    首先,我们来看一下JDK源码中对于ThreadLocal的解释:

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

    翻译过来就是:

    ThreadLocal用来提供线程内的局部变量。这些变量在多线程环境下访问时能够保证各个线程里的变量相对独立于其它线程内的变量,ThreadLocal实例通常来说都是private static类型的。

    因此,ThreadLocal适用于满足下面条件的场景:

    • 每个线程 有且仅有 该对象的一个实例
    • 在该线程的整个生命周期内 有多处用到 该实例
    • 存在 多线程访问 的情况

    1.2 ThreadLocal 的使用

    ThreadLocalAPI很简单,它包含以下四个签名:

    • get:获取ThreadLocal中当前线程共享变量的值。
    • set:设置ThreadLocal中当前线程共享变量的值。
    • remove:移除ThreadLocal中当前线程共享变量的值。
    • initialValueThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

    我们用下面的一小段例子,来熟悉一下ThreadLocal的使用。

    class ThreadLocalSamples {
    
        private static ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>() {
    
            @Override
            protected Integer initialValue() {
                return 5;
            }
    
        };
    
        static void startSample() {
            for (int i = 0; i < 3; i++) {
                new SampleThread("thread_" + i).start();
            }
        }
    
        private static class SampleThread extends Thread {
    
            private String mThreadName;
    
            SampleThread(String threadName) {
                mThreadName = threadName;
            }
    
            @Override
            public void run() {
                for (int j = 0; j < 5; j++) {
                    try {
                        long sleep = (long) (Math.random() * 50);
                        Thread.sleep(sleep);
                        int result = sThreadLocal.get();
                        sThreadLocal.set(++result);
                        Log.d("ThreadLocalSamples", "ThreadName=" + mThreadName + ",result=" + result);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    运行结果:

    运行结果
    从打印的结果可以看到,虽然这3个线程访问是同一个ThreadLocal实例,但是它们通过ThreadLocalget/set方法读写的并不是同一个实例,所以保证了在多线程环境下的独立性。

    二、源码

    2.1 源码实现

    为了加深对于ThreadLocal的理解,我们来分析一下它的内部实现。ThreadLocal设计的核心思想就是:每一个Thread维护一个ThreadLocalMapThreadLocalMapkeyThreadLocal,而value就是真正要存储的Object。这种方案设计的优点是:

    • 每个MapEntry数量变小了,之前是Thread的数量,现在是ThreadLocal的数量,能提高性能。
    • Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

    我们先来看一下setget的主要流程:

        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null)
                    return (T)e.value;
            }
            return setInitialValue();
        }
    
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    

    写入的流程为:

    • 通过静态方法currentThread获取当前执行指令的线程。
    • 得到该线程的私有成员变量threadLocals,其类型为ThreadLocalMap,如果没有创建那么就先创建。
    • 通过ThreadLocalMapset方法存入实际的Object,其key值为ThreadLocal实例。

    读取的流程为:

    • 通过静态方法currentThread获取当前执行指令的线程,然后获取和该线程关联的ThreadLocalMap
    • ThreadLocal实例为key值,通过ThreadLocalMapgetEntry方法找到Object,如果找到就直接返回;如果没有找到就调用setInitialValue方法,该方法会调用到我们重写的initialValue来尝试获取一个初始值。

    总结下来就是:ThreadLocal将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦

    它之所以可保证 多线程环境下的相互独立,原因在于:每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,线程可以正确的访问到自己的对象。

    当然,这种 独立性必须要基于一个前提通过set方法存储的对象并不是多个线程共享的。如果是共享的,那么多个线程get出来的是同一个是实例,仍然会存在多线程问题。

    2.2 ThreadLocalMap

    ThreadLocalMapThreadLocal中的一个内部类,与HashMap类似,它也会遇到Hash冲突的问题,HashMap采用了 链地址法 解决冲突,而ThreadLocalMap则采用 开放寻址法 解决冲突。

    关于ThreadLocalMap还有一个疑问,就是它有可能会出现内存泄漏,原因是:ThreadLocalMapkey值保存的是ThreadLocal的弱引用,假如ThreadLocal被回收,那么就会无法通过Key找到Object,假如线程一直没有结束,那么这些Object就永远不会被回收。

    ThreadLocalMap内部对于这种情况做了优化,就是在getEntryset方法查找存储位置的时候,如果发现了keynull的槽,那么会将这些槽中对应的Object引用置为null。这并不能解决所有问题,对于使用者来说,可以做额外的两项优化操作:

    • 手动调用ThreadLocalremove函数,删除不再需要的ThreadLocal
    • ThreadLocal声明为private static的,使得ThreadLocal的生命周期更长。

    参考文献

    (1) 正确理解 ThreadLocal
    (2) 深入剖析 ThreadLocal 实现原理以及内存泄漏问题
    (3) ThreadLocal 和 synchronized 的区别

    相关文章

      网友评论

      • 努力的菜鸟:问个问题,将ThreadLocal声明为private static的,使得ThreadLocal的生命周期更长。这个为什么可以解决内存泄漏,声明为static 对于gc回收来说,他要回收的对象,应该还是被引用了。。还是不会被收回吧
        努力的菜鸟:@泽毛 额,了解了,我只考虑到了value
        泽毛:@努力的菜鸟 这里的泄露指的是 value 的泄露,因为 key 被回收了,之后通过 key 就找不到这个 value,但是 value 依然存在内存里面,所以要保证 key 不被回收
      • 努力的菜鸟:天天看一篇。。。就怕你不写

      本文标题:多线程知识梳理(9) - ThreadLocal

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