zoukankan      html  css  js  c++  java
  • 多线程 ThreadLocal 是什么?有哪些使用场景?

    ThreadLocal常用API

    1. void set (Object value)设置当前线程的线程局部变量的值。
    2. public Object get() 该方法返回当前线程所对应的线程局部变量。
    3. public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它 可以加快内存回收的速度。无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法
    4. protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为 了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。
    5. public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE 代表一个能够存放 String 类型的 ThreadLocal 对象。 此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

     

    为什么说“get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的”?

    【这里写的不确定,请大家斧正】以get()为例,只有当被置为null的ThreadLocal自己调用get时,才会走入getEntryAfterMiss()的逻辑。其他ThreadLocal变量调用get不会清除。

    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();
        }
    
    private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                    return e;
                else
                    //清除key为null的Entry
                    return getEntryAfterMiss(key, i, e);
            }

     

    ThreadLocal适用场景

    ThreadLocal在spring的事务管理,包括Hibernate的session管理等都有出现,在web开发中,有时会用来管理用户会话 HttpSession,web交互中这种典型的“一请求一线程”的场景似乎比较适合使用ThreadLocal,但是需要特别注意的是,由于此时session与线程关联,而tomcat这些web服务器多会采用线程池机制,也就是说线程是可复用的,所以在每一次进入的时候都需要重新进行set,或者在结束时及时remove。

    ThreadLocal底层实现原理

    框架图

    • 重点:记住这张图!!!!
    • Entry里的key保存的是Threadlocal的弱引用。
    • Entry里的value保存的是强引用,保存的也是引用,不是实例。

     

    用get()方法描述底层原理

    总结

    get 方法,其实就是拿到每个线程独有的 ThreadLocalMap,然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可 以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初 始化等工作。 

    详细解释

    上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型 成员,所以 getMap 是直接返回 Thread 的成员。

    看下 ThreadLocal 的内部类 ThreadLocalMap 源码: 

    可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了 两个信息,一个是 ThreadLocal<?>类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。

    ThreadLocal引发的内存泄漏

    总结

    ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。 

    • JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
    • JVM 利用调用 remove、get、set 方法的时候,回收弱引用。 
    • 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
    • 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。

    详细分析

    根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身(应该是 ThreadLocal 实例的虚引用),value 是真正需 要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用ThreadLocal的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。 因此使用了 ThreadLocal 后,引用链如图所示:

    图中的虚线表示弱引用。

    这样,当把 threadlocal 变量(Thread Local Ref)置为 null 以后,没有任何强引用指向 threadlocal 实例(红色方块 Thread Local),所以 threadlocal实例将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现

    key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前 线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。

    只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开, Current Thread、Map value 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。

    所以回到我们前面的实验场景,场景 3 中,虽然线程池里面的任务执行完毕 了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出,我们 set 了线程的 localVariable 变量后没有调用 localVariable.remove()方法,导致线程池里面的 5 个 线程的 threadLocals 变量里面的 new LocalVariable()实例没有被释放。

    其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时 候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。

    从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用? 

    为什么使用弱引用而不是强引用? 

    从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用。

    下面我们分两种情况讨论:

    1. key 使用强引用:对 ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除, ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
    2. key 使用弱引用:对 ThreadLocal 对象实例的引用被被置为 null 了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都 有机会被回收。

    比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。

    因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。 

    错误使用 ThreadLocal 导致线程不安全

    错误原因:Entry里的value保存的是强引用,保存的也是引用,不是实例。因此不同的线程在ThreadLocalMap保存的还是引用。由于这里保存的是一个static的变量,因此不同的线程保存的value引用都指向同一个对象。

    修改方法:把number的static去掉。

    详细解释:

    为什么每个线程都输出 5? 难道他们没有独自保存自己的 Number 副本吗? 为什么其他线程还是能够修改这个值?

    仔细考察 ThreadLocal 和 Thead 的代码, 我们发现 ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其 他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有 的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出 一样的结果:5 个线程中保存的是同一 Number 对象的引用,在线程睡眠的时候, 其他线程将 num 变量进行了修改,而修改的对象 Number 的实例是同一份,因 此它们最终输出的结果是相同的。

    而上面的程序要正常的工作,应该的用法是让每个线程中的 ThreadLocal 都 应该持有一个新的 Number 对象。 

    ThreadLocal在Spring事务管理中的应用 

    Spring如何处理模板类的Bean(JDBC etc...),在多线程下的并发安全问题?

    适用ThreadLocal。。。TODO: find answer from ppt、MP4

    参考

    谈谈Java中的ThreadLocal: https://www.cnblogs.com/chengxiao/p/6152824.html

    ThreadLocal在Spring事务管理中的应用:https://www.cnblogs.com/fishisnow/p/6396989.html

  • 相关阅读:
    swift 第十四课 可视化view: @IBDesignable 、@IBInspectable
    swift 第十三课 GCD 的介绍和使用
    swift 第十二课 as 的使用方法
    swift 第十一课 结构体定义model类
    swift 第十课 cocopod 网络请求 Alamofire
    swift 第九课 用tableview 做一个下拉菜单Menu
    swift 第八课 CollectView的 添加 footerView 、headerView
    swift 第七课 xib 约束的优先级
    swift 第六课 scrollview xib 的使用
    swift 第五课 定义model类 和 导航栏隐藏返回标题
  • 原文地址:https://www.cnblogs.com/frankcui/p/10820066.html
Copyright © 2011-2022 走看看