一.为什么要有ThreadLoacl这个东西?
首先我们先看一段原生的JDBC的代码
我们在使用jdbc的时候,首先要配置完成后再拿到JDBC连接,然后在增删改查业务方法里拿到这个连接,然后把sql放送给DB去执行。
在我们公司真实的开发代码的场景中肯定不可能是每一次都临时的去拿连接,而是通过数据库连接池去管理数据库的连接,有些更新数据的业务还要开启一些事物,于是代码就变成下面这个
这样也有问题,执行插入的是一个数据库连接,执行getAll方法的可能又是一个数据库连接,两个数据库连接不一样,遇到问题是不可能回滚的,因为事物失效了,是可以用分布式事物解决,但是我们如果仅仅是一个单机服务,在java中如何解决?最简单的办法就是把每个方法的参数都加一个连接传进去呗。
但是我看SSM的代码从来没有把数据库连接作为参数传递的,包括我们自己也不可能在业务代码中把数据库连接当作参数进行传递。而在springboot项目中我们经常用声明式事物注解来对一个方法或者一个类的业务操作进行管理。
DataSourceTransactionManager这个类里面有一个doBegin方法,里面有一个bindResource方法
所以,Spring使用了一个ThreadLocal把连接绑定到线程的。
Thread为每一个变量提供了一个单独的线程副本,使得每一个线程在同一时刻访问的并非同一个对象,这样就隔离了多个线程对数据的共享。
二.ThreadLocal和Synchronized的区别
他们两个有本质上的区别,Synchronized是保证了某一个变量或者代码块在同一时刻对同一资源只有一个线程可以访问,使用了锁机制,而ThreadLoacl是用了副本机制,此时无论多少线程访问都是安全的。
在微服务的场景下,链路追踪中的traceId传递也是用了ThreadLocal。
三.使用
使用比较简单,一个类直接看
需要注意的就是创建的ThreadLocal一直是static的,因为其实多个线程中的map中对应的ThreadLocal对象一个就够用了,不同线程对应的是栈中的引用不同,如果不是static唯一的会浪费内存,多个map保证多个线程的值不一样显然是多余的。
四.实现原理
4.1性能分析
在《并发编程实战》众douglee做过实战
Threadlocal的性能远远高于另外两个,即使换成ConcurrentHashMap性能也不如ThreadLocal,因为ThreadLocal是让变量副本跟随着线程本身走而不是保存在某一个地方,这样在存取的时候就避免了线程之间的竞争。而且一个线程可能有多个副本,所以在线程内部就需要一个容器来保存多个副本。
4.2具体实现
首先,Thread类里面有一个成员变量是ThreadLocalMap
它是ThreadLocal的一个静态内部类,是开发人员处理的我们不需要关心,我们只需要处理Threadlocal就行,
ThreadLocalMap有一个静态的内部类Entry,它是一个弱引用,Entry的key是我们的ThreadLocal对象,value就是要隔离的副本变量,这个Entry是个数组是因为可能有多个变量需要线程隔离访问。
我们使用ThreadLocal的时候,get方法其实就是拿到每一个线程独有的ThreadLocalMap,key就是我们的ThreadLocal对象。
五.引发内存泄漏分析
Object object = new Object();这是个强引用,有指针指向的时候不可能被gc回收
但是注意我们的ThreadLoacl里面的entry是个弱引用,他继承了WeakReference,继承这个类的是一个弱引用,下一次gc发生的时候就会被清楚。
继承了SoftReference的类是一个软引用,用来描述一些有用但并非必须的对象,在内存溢出之前会把这些对象列进回收的范围进行回收,如果还是没有足够的内存就会抛出内存溢出的异常。
虚引用是幽灵引用或者幻影引用,最弱的一种引用对象关系,为一个对象设置虚引用的目的就是能够在这个对象实例被回收时能够收到一个系统通知。
5.1内存泄漏分析
每一个线程维护了一个自己的threadlocalMap,这个key就是ThreadLocal对象本身,value就是所需要的变量副本,ThreadLocal本身是不会存值的,只是他作为一个key可以让线程从map中获取对象的副本变量,而这个map是使用ThreadLocal的弱引用作为key的,在gc的时候会被回收掉。
当我们把ThreadLocal置为null以后,没有任何强引用指向ThreadLocal实例,那map将会出现key为null的Entry,这就造成了没有办法访问这些value,如果你这个线程不结束的话,这些key为null的Entry的value就会一直存在一条强引用链,造成了存在内存泄露的风险。
只有在当前线程结束以后,当前线程不在栈中,强引用断开,这些value全部被回收,最好的方法就是不在使用ThreadLocal以后调用他的remove方法删除掉。
5.2key为什么使用弱引用
如果key是强引用,那么Threadlocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,对象实例不会被回收,会导致内存泄露。
如果key用的是弱引用,,那么Threadlocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的弱引用就会被gc自动回收,value的下次get,set或者remove都有机会回收,不会导致内存泄露
原因总结:ThreadLocalmap的生命周期跟Thread一样长,你如果没有手动删除这些key,就很容易造成内存泄露。
5.3总结
1.JVM利用设置ThreadLocalmap的key是弱引用,来避免内存泄露
2.JVM利用调get,set,remove方法来回收这些弱引用
3.当ThreadLocalMap存储了很多key为null的Entry的时候,不调用remove方法容易造成内存泄露
4.使用线程池和ThreadLocal时要小心,因为这种情况下,线程是不断的重复运行,从而导致value可能造成累计的情况。
5.我们跨方法的时候合理的使用ThreadLocal也是可以的
评论区