• ThreadLocal



    前言:
    再写博客时,遇到了如何处理保存用户的信息时出现混乱的问题(线程安全问题),在一番查找后,寻到了ThreadLocal用来保存登录的用户的信息的方法。


    一、ThreadLocal是什么

    在讲ThreadLocal前,先来看一下什么是线程封闭

    封闭对应的是开放,所谓线程开放就是类似共享数据、共享变量这些概念。
    多线程访问共享可变数据时,涉及到线程间数据同步问题。并不是所有时候,都要用到共享数据,所以线程封闭的概念就提出来了。

    当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭。它是实现线程安全最简单的方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性。即使被封闭的对象本身不是线程安全的。

    那么,要如何维持线程封闭呢,这时我们就可以使用ThreadLocal来使线程中的某个值与保存值的对象关联起来。

    ThreadLocal说简单点就是往一个线程存数据,这个数据只能由这个线程获取,其他线程无法获取这个数据
    ThreadLocal就是用空间换时间,Synchronized就是时间换空间

    在这里插入图片描述
    此图对于理解ThreadLocal十分重要


    二、ThreadLocal作用

    public class th {
        private String content;
    
        private String getContent() {
            return content;
        }
    
        private void setContent(String content) {
            this.content = content;
        }
    
        public static void main(String[] args) {
            th demo = new th();
            for (int i = 0; i < 5; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        demo.setContent(Thread.currentThread().getName() + "的数据");
                        System.out.println("-----------------------");
                        System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                    }
                });
                thread.setName("线程" + i);
                thread.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述
    在实际运行后,便出现了如上的结果,线程之间不隔离,每个线程获取的数据出现了错误,使用ThreadLocal,便可以很好的解决此问题。

    ThreadLocal作用便是提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

    1. 线程并发: 在多线程并发的场景下
    2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
    3. 线程隔离: 每个线程的变量都是独立的,不会互相影响

    三、ThreadLocal的设计结构

    早期:

    在这里插入图片描述

    现在:

    在这里插入图片描述

    每个Thread维护一个ThreadLocalMap,这个Map的keyThreadLocal实例本身,value才是真正要存储的值Object

    每个Thread线程都有一个ThreadLocalMap(Map结构,但是不是直接使用的Map)
    ThreadLocalMap中储存了key(ThreadLocalMap对象)和value(Object)
    ThreadLocalMap由ThreadLocal维护,意思就是由ThreadLocal负责获取和设置 每一个线程互不干扰,形成隔离

    好处:

    • 每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
    • Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

    四、ThreadLocal核心方法

    静下心来看源码

    1. set方法

        public void set(T value) {
            Thread t = Thread.currentThread();  //获取当前线程对象
            ThreadLocalMap map = getMap(t);  //获取此线程对象中维护的ThreadLocalMap对象
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
        
        void createMap(Thread t, T firstValue) {
             //  this是调用此方法的threadLocal
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2. get方法

        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;   // 取值
                    return result;
                }
            }
            return setInitialValue();   // 设置初始化值
        }   
        private T setInitialValue() {
            T value = initialValue(); // 获取初始化值 初为null,可重写
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t); // 获取此线程对象中维护的ThreadLocalMap对象
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
            if (this instanceof TerminatingThreadLocal) {
                TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
            }
            return value;
        }
    
        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
                    return getEntryAfterMiss(key, i, e);
            }
            
         protected T initialValue() {
               return null;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    3. remove方法

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null) {
                 m.remove(this);
             }
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    五、ThreadLocal内存泄漏

    强引用:只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了.

    软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

    弱引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。

    虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。

    内存泄漏:指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    在这里插入图片描述
    一个线程属于强引用,对于一整个生命周期
    当key为强引用时,key和value会一直存在,Entry不会被回收,从而导致内存泄漏
    当key为弱引用时,在线程的生命周期中进行了一次垃圾回收,此时key作为弱引用被干掉了,但是整个线程还是存在,此时在Map中只有value,便会永远存在于内存中,当有大量的此行为时,便可能会造成内存泄漏

    我们得知两个引用都有风险,但是为什么选择了弱引用呢?
    我们是通过ThreadLocal来对ThreadLocalMap进行操作的,假设使用ThreadLocal的对象被设置为null了,那ThreadLocalMap的强引用指向ThreadLocal也毫无意义。弱引用反而可以预防大多数内存泄漏的情况,毕竟被回收后,下一次调用set/get/remove时ThreadLocal内部会清除掉


    六、使用场景

    1.用于保存线程不安全的工具类,如SimpleDateFormat
    2.每个线程需要独立保存信息,如个人博客中保存的用户信息


    七、参考资料

    https://www.zhihu.com/question/399087116/answer/2763547159
    https://www.bilibili.com/video/BV1QS4y1s7hi
    https://blog.csdn.net/LemonSnm/article/details/122251312
    https://www.cnblogs.com/liyutian/p/9690974.html

  • 相关阅读:
    【Text2SQL 论文】DBCopilot:将 NL 查询扩展到大规模数据库
    java毕业生设计在线党建学习平台计算机源码+系统+mysql+调试部署+lw
    微调 Florence-2 - 微软的尖端视觉语言模型
    ABTest样本量计算
    Jasper 中如何将数据拆成多行并跨行累计
    (三) CPU 性能测试 (CPU负载高对应的不同情况)
    如何在 PyTorch 中冻结模型权重以进行迁移学习:分步教程
    Quartz入门——定时任务动态添加修改及持久化与自动恢复
    ceph 007 双向池同步 rgw对象网关配置 s3对象存储
    一文详解处理器缓存(缓存一致性、读写屏障、指令预取、false sharing)
  • 原文地址:https://blog.csdn.net/weixin_63802890/article/details/128058401