ThreadLocal
1. Java内存模型
- Java所有变量都存储在主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写,线程修改完变量之后要刷新到主内存中;
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
线程1对共享变量的修改,要想被线程2及时看到,必须经过如下2个过程:
①把工作内存1中更新过的共享变量刷新到主内存中
②将主内存中最新的共享变量的值更新到工作内存2中
2. 背景
多个线程对一个变量进行写入的时候容易产生线程安全问题
,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性;
而同步锁可以保证共享资源在同一时间内只能由一个线程去访问,这样就能解决线程安全问题(数据共享);
ThreadLocal 是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法;
思路:让每个线程对共享变量进行访问的时候,访问的都是线程自己的变量,这样就不会有线程安全问题了。(以数据隔离的方式来保证线程安全)
3. 概述
ThreadLocal为每一个线程都提供一个专属的变量副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突;
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本;
可以使⽤ get() 和 set() 方法法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题;
例:
ThreadLocal的数据是隔离的,每个线程都能拿到变量的副本,线程2 拿到的还是null !
4. 结构
JDK8之前: key是Thread对象,由Threadlocal对象来维护ThreadlocalMap;
JDK8改变结构的好处:
1.Thread作为Key,则在线程很多时数组元素Entry会很多,而ThreadLocal作为Key,Entry数量会减少很多;key少了就能尽量避免哈希冲突
2.当Thread销毁时,ThreadLocalMap也会随之销毁,减少内存开销;而早期ThreadLocalMap由ThreadLocal维护,线程结束并不会销毁ThreadLocalMap;
JDK8中结构: key是threadLocal对象,由Thread线程对象来维护threadLocalMap;
结构说明:
每个Thread维护一个ThreadLocalMap,
Key是Threadlocal对象,value就是Threadlocal的set方法存的值(变量副本);
对于不同的线程,有着各自不同的ThreadLocalMap,所以就能使数据副本的隔离;
5. set() 、get()、remove()方法源码
5.1 set()
1.先获取当前线程对象;
2.获取线程对象的threadLocals属性,也就是threadLocalMap;
3.如果map存在就将当前threadLocal对象作为key,传入的值作为value传入map; 如果map为空就会创建一个map;
5.2 get()
1.获取当前Thread线程对象;
2.获取线程的threadLocals即threadLocalMap;
3.当map【存在】,以当前的threadLocal对象为key,调用map.getEntry方法拿到Entry实体,若Entry实体不为空则取出Entry试题中的value;
4.①如果map【不存在】 或 ②没有与当前threadLocal对象关联的Entry,则会调用setInitiavalue()设置初始化方法:
先得到value,默认为null,
判断map,如果为空则创造map;
如果map不为空则把当前ThreadLocal对象作为key,和value 存入 map
5.3 remove()
1.获取当前线程对象;
2.获取线程的threadLocals即threadLocalMap;
3.如果map不为空 就移除当前threadLocal对象(Key)关联的Entry;
6. 内存泄露问题
6.1 概念
Memory overflow 内存溢出:没有足够的内存提供申请者使用;
Memory lead 内存泄露:已经动态分配的堆内存因某种原因未释放或无法释放,造成系统内存的浪费,内存泄漏的堆积最终将导致内存溢出;
强引用:常见的普通对象引用,只要还有强引用指向一个对象,就能表名对象还“活着”,垃圾回收器就不会回收这种对象
弱引用:垃圾回收器一旦发现只具有弱引用的对象,不管内存是否租足够,都会回收它的内存;
6.2 分析
假设使用强引用:
即key强引用threadLocal;
当代码中使用完ThreadLocal, ThreadLocal的 引用 就被回收,但是由于ThreadLocal 对象 被key强引用,造成ThreadLocal对象无法被回收!
且由于存在强引用链:thread引用→thread对象→threadLocalMap(包含key)→Entry,所以只要线程还存在,Entry就无法被回收, 也就是说如果ThreadLocal的key使用了强引用是无法避免内存泄漏的!
使用弱引用:
此时key是弱引用指向threadLocal对象,当代码中使用完了threadLocal,threadLocal的引用会被回收掉,而此时threadLocal 对象(作为Entry的key)被gc回收,此时Entry的key就成了null;
而由于存在强引用链条:thread引用→thread对象→threadLocalMap→Entry,即只要线程还存在,Entry就无法被回收,所以此时存在key为null
,而value有值的Entry,这个Entry无法被访问,会造成内存泄漏;
但使用弱引用至少threadLocal对象可以被回收;
内存泄露的根本原因 ?
因为ThreadLocalMap是Thread的属性,生命周期和Thread一样,当线程对象还存活时,如果没有手动删除key为空的Entry,就会造成内存泄漏;
为什么Key要弱引用?
1.可以解除threadLocal对象和线程生命周期的绑定,key弱引用threadLocal对象,则一旦threadLocal使用完可以直接被回收,而不用等到线程结束;
2.方便避免内存泄漏:如果是弱引用,entry的key会被设置成null,【在使用set 和get 的时候】,会对key为null进行判断,如果为null,那就对value设置为null。以避免内存泄露;
即就算忘记remove,则也会多一层保障,更安全;
如何避免内存泄露 ?
1.使用完ThreadLocal,调用 remove() 方法删除对应的Entry; (主动)
2.使用完ThreadLocal,线程也结束;(难控制,线程池中线程不会消失)
7. 哈希冲突的解决
1.首先根据key算出索引 i,查找 i 位置的Entry
2.若Entry已经存在,并且key等于传入的key,直接给这个Entry赋新值;
3.若Entry已经存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry
4.不断循环检测,直到遇到null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新加一个Entry,同时size+1
5.最后调用cleanSomeSlots,清理key为null的Entry,返回是否清理了Entry,如果size达到2/3的阈值,就rehash进行全表扫描清理;
ThreadLocalMap 使用线性探测法来解决哈希冲突:
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出;
如:
如果数组长度是16(初始未扩容),计算出的key是14,而此时索引14已经有值了,那么将14+1=15,看索引15的位置有没有值,如果还是冲突就 回到索引0,看看0位置有没有值,直到可以插入;
即可以将Entry[]看作一个环形数组;