一.简单说一下ThreadLocal
1.ThreadLocal是一个线程变量,用于在并发条件下,为不同线程提供相互隔离的变量存储空间。在多线程并发的场景下,每个线程往ThreadLocal中存的变量都是相互独立的。
2.基本方法
(1)set(Object v):将变量与当前线程绑定。
(2)get():获取当前线程绑定的变量
(3)remove():移除当前线程绑定的变量
二.synchronized和ThreadLocal的区别
synchronized和ThreadLocal都是用于解决多线程并发访问共享变量的情况。
1.原理:
(1)synchronized是通过加锁的方式,只提供一份变量,每次只让一个线程访问该变量;上一个线程对变量的访问结果会影响下一个访问变量的线程。
(2)ThreadLocal是对变量进行拷贝,每一个线程都往自己的空间中存一份共享变量的副本,多线程可同时访问各自的副本变量。且由于线程空间相互隔离,线程对副本变量的修改不会对其他线程产生影响。
2.侧重点:
(1)synchronized主要用于多线程间访问资源的同步,保证了并发编程的原子性和可见性;但并发效率下降。
(2)ThreadLocal主要用于同一个线程在上下执行逻辑间传递数据,保证了多线程间数据的隔离。
三.说一下ThreadLocal的内部结构
1.jdk1.7:
(1)一个ThreadLocal内部有一个Map(ThreadLocalMap)用于存储数据。
(2)ThreadLocalMap的key是线程Thread,value是该线程绑定的变量副本。
(3)线程每次要访问变量,都要到ThreadLocal中,以自己为映射找到对应的变量。
2.jdk1.8:
(1)每一个线程Thread内部都有一个Map(ThreadLocalMap)用于存储数据。
(2)ThreadLocalMap的key是ThreadLocal,value是该线程绑定的副本变量。
(3)线程每次要访问变量,都要到自己的空间中,以当时绑定的ThreadLocal对象作为key映射,找到对应的变量。
3.jdk1.8把ThreadLocalMap存在Map中的好处
(1)降低哈希冲突的概率。jdk1.8的ThreadLocalMap是用ThreadLocal对象作为key,jdk1.7的ThreadLocalMap是用Thread对象作为key,通常程序中ThreadLocal对象的数量要明显少于Thread对象的数量,则ThreadLocalMap要存储的元素就减少,发生哈希冲突的概率就会下降。
(2)提高内存利用率。ThreadLocalMap在Thread中,则当Thread销毁时对应的ThreadLocalMap也会被销毁。但若是ThreadLocalMap在ThreadLocal中,则只要ThreadLocal还存在,即使Thread都销毁了,对应的ThreadLocalMap仍然存在占据空间。
四.说一下ThreadLocal的底层原理
1.set(T value):
(1)获取当前线程
(2)如果当前线程的ThreadLocalMap不为空,则以键值对ThreadLocal:value存入。
(3)如果当前ThreadLocalMap为空,则先创建一个ThreadLocalMap,再将ThreadLocal:value存入。
2.get():
(1)获取当前线程
(2)如果当前线程的ThreadLocalMap不为空,则以ThreadLocal为键,索引到对应节点Entry e
(3)如果e不为null,则直接将e返回
(4)如果当前线程的ThreadLocalMap为null 或 获取到的e为null,则调用setInitialMap()方法:若ThreadLocalMap为null,则创建ThreadLocalMap,并将键值对ThreadLocal:initialValue()存入后返回;如果e为null,则将键值对ThreadLocal:initialValue()存入后返回。即最终返回的都是initialValue()。
3.initialValue():
(1)直接返回一个null
(2)也就是说对于get()方法中ThreadLocalMap为null或者索引到的Entry为null时,都会以ThreadLocal:null存入,并将null返回;只有当第一次调用set(T value)时才会将null值覆盖。
(3)可以通过重写initialValue()方法,设置存入的初始值不为null。
4.remove():
(1)获取当前线程
(2)如果当前线程的ThreadLocalMap不为null,则将索引到的Entry移除。
五.说一下ThreadLocal的内存泄露情况
1.ThreadLocalMap的引用情况:
(1)ThreadLocalMap是Thread的一个属性,Thread对ThreadLocalMap是强引用;ThreadLocalMap对里面的每一个节点Entry也是强引用。即存在强引用链:Thread——ThreadLocalMap——Entry。
(2)ThreadLocal是ThreadLocalMap的键,ThreadLocalMap对ThreadLocal是弱引用。
2.如果ThreadLocalMap对ThreadLocal是强引用:
(1)当栈中其他对ThreadLocal的引用销毁,由于ThreadLocalMap对ThreadLocal是强引用,因此ThreadLocal在堆中无法被回收,有OOM的风险。
(2)栈中其他对ThreadLocal的引用销毁,则ThreadLocalMap中该ThreadLocal对应的Entry也就没有意义;但由于ThreadLocalMap对Entry是强引用,因此该Entry无法被回收,有OOM的风险。
3.如果ThreadLocalMap对ThreadLocal是弱引用:
(1)当栈中其他对ThreadLocal的引用销毁,由于ThreadLocalMap对ThreadLocal是弱引用,因此ThreadLocal在堆中可以被回收。回收后ThreadLocalMap对应的键就会变回null,但值Entry仍然存在。
(2)ThreadLocal被回收,则ThreadLocalMap中该ThreadLocal对应的Entry也就没有意义;但由于ThreadLocalMap对Entry是强引用,因此该Entry无法被回收,有OOM的风险。
4.可以看出:无论ThreadLocalMap对ThreadLocal是强引用还是弱引用,都会有OOM的风险,因为ThreadLocalMap会导致OOM的根本原因是存在强引用链Thread——ThreadLocalMap——Entry,导致Entry的生命周期和Thread一样长,若不手动删除对应的Entry就会有OOM的风险。
5.因此使用ThreadLocal时为了避免OOM,一定要调用remove()手动删除对应的Entry。
六.无论ThreadLocalMap对ThreadLocal是强引用还是弱引用都会导致OOM,那选择弱引用的原因是什么?
1.ThreadLocalMap对ThreadLocal是弱引用,则当外部没有其他对ThreadLocal的强引用时,堆中的ThreadLocal可以被顺利回收。提高内存利用率。
2.ThreadLocal被回收后,其在ThreadLocalMap中对应的键会变成null,变成了幽灵键。而ThreadLocal的set()、get()、remove()等方法在执行时会进行迭代检查,若检测到幽灵键则会将对应的Entry移除。降低了OOM的风险。
七.ThreadLocalMap中解决哈希冲突的方式是什么?
线性探测法
1.若将ThreadLocal作为key哈希映射到的槽位已被占用,且占用槽位的ThreadLocal和当前ThreadLocal不是同一个对象,则当前索引加一,继续判断下一个。若索引到达最后一个仍不满足,则索引归零,重新开始往后判断,直到找到一个槽位为null,可存储;或者找到占用槽位的ThreadLocal和当前ThreadLocal为同一个对象,可覆盖。
2.ThreadLocalMap的扩容阈值threshold是length*(2/3),因此不会出现把整个Map都遍历一遍仍然找不到槽位存储的情况。