• threadlocal


    什么是ThreadLocal

    线程本地存储技术,能够做到线程隔离

    为什么要使用ThreadLocal?

    同一个线程里面,可以跨对象的去共享数据,同时保证线程安全,每个线程只访问自己的数据

    怎么使用?

    ThreadLocal threadLocal = new ThreadLocal();

    threadLocal.set(value);   存储线程隔离的数据

    threadLocal.get();   获取当前线程存储的数据

    使用场景

    如果想在同一个对象中,跨多个对象的去共享数据,同时保证线程安全的情况下,使用ThreadLocal

    比如:

    用户登录成功后,需要将登录用户信息保存起来,以方便在系统中的任何地方都可以使用到,那么此时就可以使用ThreadLocal来实现。例如:Spring Security中的ThreadLocalSecurityContextHolderStrategy类

    比如

    在Tomcat中,如果我们想在 filter,controller,service之间方便的去共享数据,并且保证每个请求只能访问自己的数据,此时可以使用ThreadLocal

    工作原理

    每个线程对象中都有一个自己的 ThreadLocalMap 对象,每个线程负责把自己的数据存到ThreadLocalMap 对象中,如上图,tomcat给每个请求分配一个线程(调用threadLocal.set(user1)在当前线程中存放user1数据),每个线程中有一个成员变量 threadLocals,成员变量指向每个线程中的 ThreadLocalMap 对象,当调用threadLocal.get()方法时,就可以取出当前线程的资源

    get方法

    1. public T get() {
    2. Thread t = Thread.currentThread(); //保证当前线程
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null) {
    5. ThreadLocalMap.Entry e = map.getEntry(this);
    6. if (e != null) {
    7. @SuppressWarnings("unchecked")
    8. T result = (T)e.value;
    9. return result;
    10. }
    11. }
    12. return setInitialValue();

    set方法

    1. public void set(T value) {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null)
    5. map.set(this, value);
    6. else
    7. createMap(t, value);
    8. }

    ThreadLocal为什么会内存泄漏

    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。根本原因是因为每个线程中的 ThreadLocalMap 生命周期和线程的生命周期是一样长的

    其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue

    但是这些被动的预防措施并不能保证不会内存泄漏:

    • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
    • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

    如何解决内存泄漏

    在最后要被线程池回收之前,使用 threadLocal.remove() 方法,把当前threadlocalmap中的数据清空掉

    key为什么要设置成弱引用?

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

    • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
    • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

    比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

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

    而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry(Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。)内存泄漏完全没必要过于担心。

    为什么value不是弱引用

    先说答案:假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。

    1. Map, WeakReference> map = new HashMap<>(8);
    2. WeakReference key = new WeakReference<>(1);
    3. WeakReference value = new WeakReference<>(300);
    4. map.put(key,value);
    5. System.out.println("put success");
    6. Thread.sleep(1000);
    7. System.gc();
    8. System.out.println("get " + map.get(key).get());
    9. // put success
    10. // get null

    使用ThreadLocal的目的就是要把这个value存储到当前线程中,并且希望在你需要的时候直接从当前线程中拿出来,那么意味着你的value除了当前线程对它持有强引用外,理论上来说,不应该再有其他强引用,否则你也不会把value存储进当前线程。但是一旦你把本应该强引用的value设计成了弱引用,那么只要jvm执行一次gc操作,你的value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。

    使用threadlocal的缺点

    脏读(在一个线程中读取到了不属于自己的信息就叫做脏读)

    脏读产生原因:线程池复用了线程,和这个线程相关的静态属性也复用,所以就导致了脏读

    1. //脏读问题
    2. private static ThreadLocal threadLocal=new ThreadLocal<>();
    3. public static void main(String[] args) {
    4. ThreadPoolExecutor executor=new ThreadPoolExecutor(1,1,0,
    5. TimeUnit.SECONDS,new LinkedBlockingQueue<>(1000));
    6. for (int i=0;i<2;i++){
    7. executor.submit(new MyThreadLocal());
    8. }
    9. }
    10. static class MyThreadLocal extends Thread{
    11. //静态变量被复用
    12. private static boolean flag=false;
    13. @Override
    14. public void run() {
    15. String tname=this.getName();
    16. if(!flag){
    17. //第一次执行
    18. threadLocal.set(tname);
    19. System.out.println(tname+"设置了"+tname);
    20. flag=true;
    21. }
    22. System.out.println(tname+"得到了"+threadLocal.get());
    23. }
    24. }

    解决脏读问题:

    a.避免使用静态变量

    b.使用完之后,执行remove移除

    内存溢出(配合线程池使用的时候才会出现)
    当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就是内存溢出

    1. //设置最大内存的是5M
    2. public class ThreadLocalDemo11 {
    3.     private static ThreadLocalthreadLocal=new ThreadLocal<>();
    4.     public static void main(String[] args) {
    5.         ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
    6.                 new LinkedBlockingQueue<>(1000));
    7.         for(int i=0;i<10;i++){
    8.             executor.execute(new Runnable() {
    9.                 @Override
    10.                 public void run() {
    11.                     MyThreadLocal myThreadLocal=new MyThreadLocal();
    12.                     threadLocal.set(myThreadLocal);
    13.                     System.out.println(Thread.currentThread().getName()+
    14.                             "设置了值");
    15.                 }
    16.             });
    17.         }
    18.     }
    19.     static class MyThreadLocal{
    20.         private byte[]bytes=new byte[1*1024*1024];
    21.     }
    22. }

    解决内存溢出:使用remove移除
    内存溢出原因(配合线程池使用的时候才会出现内存溢出):
    1.线程池是长生命周期,使用完后不会主动释放资源
    2.Thread–>ThreadLocalMap–>Entry[ ] key,value(1MB资源) key是弱引用,value是强引用,垃圾回收器不会回收value资源
     

  • 相关阅读:
    JavaScript算法39- 划分技能点相等的团队(leetCode:6254middle)周赛
    Scientific colour maps颜色包--共35种--全平台可用
    MongoDB基础学习(五)之在Springboot项目使用MongoTemplate进行操作
    通关宝典!Java 面试核心知识让你面试过,过,过!
    数据可视化之百变柱状图
    layui实现鼠标移入/移出时显示/隐藏tips
    「PaddleOCR」 模型应用优化流程
    PHP即刻送达同城派送小程序系统
    Android通过jni调用本地c/c++接口方法总结
    5个 Istio 访问外部服务流量控制最常用的例子,你知道几个?
  • 原文地址:https://blog.csdn.net/m0_46628950/article/details/126448853