ThreadLocal作用
ThreadLocal是Java中用于解决线程安全问题的一种机制,它允许创建线程局部变量,即每个线程都有自己独立的变量副本,从而避免了线程间的资源共享和同步问题。
ThreadLocal底层原理
每一个线程内部有一个ThreadLocalMap,它其实就是一个HashMap,底层用一个Entry数组来存数据。Entry的key是ThreadLocal本身,弱引用;value就是要保存的一个资源,强引用。

- set()方法:以当前的ThreadLocal对象为key,以资源为value,然后放到ThreadLocalMap中
- get()方法:以当前的ThreadLocal对象为key,从ThreadLocalMap取值
开发中一般把ThreadLocal声明为static类型的变量,此时ThreadLocal对象就是所有线程共享的。
那么问题来了:
1.既然key都是共享的,那不同线程取出来的value不就是同一个了吗?
其实不是,ThreadLocalMap是每个线程私有的,每个线程的ThreadLocalMap不一样,各有一份。所以即使key是相同的,但是从不同ThreadLocalMap中取出来的value也不一样。
2.为什么key是弱引用的?
因为要避免key的内存泄漏问题。
正常new一个对象时,基本上都是一些局部变量,方法执行完GC就把这些对象清理回收了,如果创建一个全局的map或者list,然后一直往里面存东西不清理,那也会出现内存泄漏。一般来说,这种全局的map或者list都是本地缓存,或者其他的第三方库中会用到的,内存泄漏的情况一般第三方库都会处理好,开发人员无需处理。
我们正常去new Thread(),用完之后正常销毁,那Thread类中的ThreadLocalMap这个属性也就会被GC回收了,不会出现内存泄漏问题。但是项目中其实没有人直接new Thread(),都是直接使用线程池。Spring Boot项目处理请求时,也是从tomcat线程池中拿出来一个线程,然后去处理请求。
假设key是强引用的,由于线程池中的线程不会销毁,会一直复用,也就是说一直存在一条引用链:GC Roots -> … -> Thread -> ThreadLocalMap -> Entry -> key -> ThreadLocal,即便不想用ThreadLocal了,把ThreadLocal对象设置为null,但是这条引用链还是存在的,Entry对key的这个引用也是存在的,而这个key对应的ThreadLocal对象已经不用了,但是还是没办法被GC回收,所以要把这个key设置为弱引用。如果只被弱引用关联的话,那么只要发生了垃圾回收,它就能够被直接回收,从而避免内存泄漏问题。
3.既然key是弱引用,那么它一定会被GC回收,一定会变成null吗?
不一定,弱引用被GC回收的前提是:这个对象只有弱引用关联,没有其他的引用。如果ThreadLocal对象没有被设置为null的话,那么还有一个强引用指向它,所以得把ThreadLocal对象设置为null,强引用才会断开,就只剩下Entry的key的弱引用,GC才能自动回收。
4.ThreadLocalMap会不会出现大量key为null的数据?
当我们去调用ThreadLocal的get(), set()方法或者remove()方法的时候,底层就会遍历Entry数组,清理掉key为null的无效的Entry,然后通过线性探测法重新去处理哈希冲突。
5.为什么value不是弱引用的?
我们把数据存到value中是为了后面去用它,如果value是弱引用的,那么现在代码里可能还没有用value,也就是说暂时还没有强引用去指向这个value,那么GC之后,就会错误地把这个value回收了。但是value后面可能还要去获取和使用,所以value不能直接被GC回收,因此必须是强引用的。
但是由于value是强引用的,因此也会存在着一条引用链:GC Roots -> … -> Thread -> ThreadLocalMap -> Entry -> value -> ThreadLocal,所以在使用完ThreadLocal之后需要调用remove()方法手动断开value的强引用,让GC将其回收。
ThreadLocal与锁
ThreadLocal和锁都是为了解决并发安全的问题,两者的区别是:
锁的思想是共享资源只有一份,强制让多个线程必须互斥访问;ThreadLocal的思想是把这个资源复制多份,每个线程一个副本,各访问各的,不冲突。
ThreadLocal的思想也是处理死锁的思想,产生死锁的四个条件是:互斥、不可剥夺、请求保持条件、循环等待条件。
- 互斥:共享资源必须互斥访问,不能被同时访问。ThreadLocal的思想就是让每个线程操作自己的数据副本,让每个线程各自访问各自的资源,因此就能够破坏互斥条件。
- 请求保持条件:多个线程各占用一份资源,同时还在请求其他线程的资源且每个线程都不愿意放弃自己的资源,从而形成互相等待,造成死锁。举个栗子,线程一和二需要获取两个锁A和B,才能够继续执行。如果线程一持有锁A,然后并尝试获取B,同时线程二持有锁B,然后尝试去获取锁A,双方互相等待就有可能产生死锁。ReentrantLock有个tryLock()方法,尝试加锁,加锁失败就可以做其他操作,通过这个其实就可以设置一种机制,如果不能一次性获取所有的锁,那就主动释放自己已持有的锁,从而打破请求保持条件。
说明
内容参考@程序员回家养猪博主的视频进行总结
网硕互联帮助中心




评论前必须登录!
注册