public class ThreadLocalTest {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalTest tt=new ThreadLocalTest();
for(int i=0;i<5;i++) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
tt.setContent(Thread.currentThread().getName()+"的数据");
System.out.println(Thread.currentThread().getName()+" ---> "+tt.getContent());
System.out.println("---------------------------------");
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
从运行结果可以看出:线程0使用了线程1的数据
解决方法1:
@Override
public void run() {
synchronized(ThreadLocalTest.class) {
// TODO 自动生成的方法存根
tt.setContent(Thread.currentThread().getName()+"的数据");
System.out.println(Thread.currentThread().getName()+" ---> "+tt.getContent());
System.out.println("---------------------------------");
}
}
缺点:效率变低了
解决方法2:
private ThreadLocal<String> tl=new ThreadLocal<>();
public String getContent() {
// return content;
return tl.get();
}
public void setContent(String content) {
// this.content = content;
tl.set(content);
}
与synchronized相比:效率变高了,可以并发执行
ThreadLocal是用来干嘛的?
从下面三个方面解释:
①线程并发:ThreadLocal应用于多线程并发的场景中
②数据传递:同一线程中的不同组件之间可以数据传递
③数据隔离:别的线程想访问当前线程的数据是不行的,数据隔离,互不干扰
ThreadLocal的应用场景:
转账是一个最典型的案例:转账可能会出现一些异常导致事务没有回滚,账户的前减少了,而另一个账户的前没有增加。这里就需要开启事务,这里就需要Service层和Dao层前后使用的Connection对象要一致,这时就可以使用ThreadLocal将对象和线程进行一个绑定,前后获取的都是同一个Connection对象。当然,也可以使用值传递和synchronized关键字进行处理,但是效率降低了且代码之间的耦合度增高了。
ThreadLocal的底层设计:
①Thread中有一个Map属性
②由ThreadLocalMap进行管理,由ThreadLocal进行设置、获取值
③Map的key为ThreadLocal对象
④不同线程想要访问当前线程的副本是不行的,数据是隔离的,互不干扰
这样设计的好处:①Entry的数量变少了②Thread销毁时,ThreadLocalMap也会进行销毁,减少内存的消耗
ThreadLocal中的常用方法:
1.initialValue():这是个延时方法,当未调用set方法之前先调用了get方法,使用此方法进行一个value的赋值
2.set():为当前线程绑定变量值
3.get(): 获取当前线程绑定的变量值
4.remove(): 移除当前线程绑定的变量值
set ()方法执行流程:
①获取当前线程的对象,根据当前线程对象获取map
②map不为空,为map设置值(key为当前ThreadLocal对象,value为传递进来的值)
③map为空,则创建一个新的map并赋值
get()方法的执行流程:
①获取当前线程对象,获取当前线程对象的map
②map存在,以ThreadLocal对象为key获取ThreadLocalMap中的Entry对象e,e不为空则返回value
③map不存在、Entry不存在则调用initialValue方法并执行创建一个map并进行赋值
remove()方法执行流程:
①获取当前线程对象,获取当前线程对象的map
②map存在,根据ThreadLocal对象为key进行移除
为了阐述下面要引出的问题,先将几个概念:
内存溢出:无法为申请者提供足够的内存
内存泄漏:程序已分配的堆内存由于程序无法释放或者不能释放,导致程序运行变慢严重系统崩溃,最终导致内存溢出
强引用:new出来的结构都是强引用,只要该对象还被引用的话,jvm就不会回收它
弱引用:jvm的垃圾回收器发现了它,不管内存是否足够,都会将其回收
这里插上一句:ThreadLocalMap中的Entry的key是弱引用
ThreadLocal内存泄漏的原因分析:
有些博客上说ThreadLocal内存泄漏的原因是因为ThreadLocalMap的Entry使用的是弱引用。我们可以用反证法进行一个证明,假设key使用强引用的话。
强引用分析:
① 当业务执行完成,将ThreadLocal对象的引用进行回收之后,由于key是强引用所以堆空间中的ThreadLocal对象无法回收
②只要没有调用remove方法手动删除Entry并且当前线程还没结束,就存在一条强引用链:currentThread ref ->currentThread --> ThreadLocalMap -->Entry,这样的话Entry就不会
被回收。由此可以看出,就算key使用强引用依旧可能会导致内存泄漏
弱引用分析:
①当业务执行完成后,ThreadLocal引用被回收,key为弱引用所以ThreadLocal对象会被回收,key会被置为null。
②如果没有手动调用remove方法移除Entry并且当前线程没有结束,始终存在一条强引用链:
currentThread ref --> currentThread --> ThreadLocalMap --> Entry --> value。key置为null所以value永远不会被访问到,会导致内存泄漏。
总结:无论key使用强引用还是弱引用都可能会导致内存泄漏,根本原因就是:ThreadLocalMap的生命周期和Tread的生命周期一致
解决ThreadLocal内存泄漏的方法:
①手动调用remove方法
②ThreadLocal使用完成后,让线程结束。显然,这种方式不好操作,因为我们使用的线程都是来自线程池中的,用完就回收了
有些同学可能会问既然强引用和弱引用都会导致内存泄漏,为什么使用弱引用?
这是因为弱引用比强引用多一层保障,使用弱引用,当ThreadLocal对象回收之后key置为null,ThreadLocalMap无论是set/getEntry都会将value置为null。就算我们忘记手动remove了,对应的value下次无论调用ThtreadLocalMap中的set、get、remove都会将其先清空。
ThreadLocalMap中的set底层是怎么实现的?
①以ThreadLocal对象为key,根据相应的算法得到数组下标i,获取i位置上的Entry
②Entry存在,Entry中的key与传入的key一致,则将新的value替换就的value
③Entry存在,key为null,调用方法将Entry进行替换
不断循环探测,直到Entry为null,之前都没return,则创建一个新的Entry并且插入,size加1
调用方法清理key为null的Entry,如果没有key为null的Entry并且size>=阈值(12)则调用rehash()
进行全表的扫描
ThreadLocalMap中的Entry存放如何避免hash 冲突的?
主要使用线性探测法,一次探测下一个地址,直到找到为null的地址,若整个空间都不满足则溢出
例如:数组的长度为16,根据相应的算法得出i=14,t[14]如果不满足,探测t[15],如果不满足则探测t[0],直至找到满足的位置。此时,数组可以看成一个环形数组