从SimpleDateFormat开始
首先看一个例子,创建20个线程,线程里就干一件事,就是转换时间
1 | java复制代码public class ThreadLoaclExample { |
运行一下,报错了
原因是什么,原因就是SimpleDateFormat是非线程安全的,点进去看一下SimpleDateFormat的源码,在类的上面就写着一段话,DateFormat不是同步的,它被推荐创建独立的format实例给每个线程,如果多线程要同时访问的话,必须在外部加一个同步的。
这段话是什么意思呢,就是解决这个问题有两个办法,一个是加synchronized,代码如下:
1 | java复制代码public static synchronized Date parse(String strDate) throws ParseException { |
但是这样做肯定会降低性能。还有一种方法就是做线程隔离,就是他注释上写的,为每个线程单独创建一个SimpleDateFormat对象,独一份的,线程独有的,这样就不会产生线程安全问题。这个就需要用到今天的主角ThreadLocal,代码如下:
1 | java复制代码public class ThreadLoaclExample { |
运行一下,不报错了
当然上面还有个优化点就是20个线程,当1000个线程的时候,每个线程都有自己独立的SimpleDateFormat副本,这样会创建1000个SimpleDateFormat对象,会很浪费空间,所以改写成线程池的方式:
1 | java复制代码public static void main(String[] args) { |
这样的话有个好处就是用16个SimpleDateFormat对象即可完成1000个任务。
以上就是第一种非常典型的适合使用 ThreadLocal 的场景。
第二种场景
第二个作用就是起到一个上下文的作用,有这样一个应用场景,当一个请求过来service-1把user的信息计算出来,后面的方法service-2,service-3,service-4都需要用到user信息,这时的做法就是把user作为参数,不停的往后传,这样的做法导致代码十分冗余。
有一个解决办法就是把user信息放在内存中,比如hashmap,这样service-1把user信息put进去,service-2,service-3,service-4直接get就能把user信息获取出来,这样可以避免把user作为参数不停的传。
那么随之而来就会产生另一个线程并发安全问题,当个线程同时请求访问的时候呢?那我们就是要使用 synchronized 或者 ConcurrentHashMap来保证hashmap的安全,它对性能都是有所影响的。
那么最终解决方案就是使用ThreadLocal,它使得每个线程独享自己的user信息,保证了线程安全,使用的时候也只要在service-1里面存进去,service-2,service-3,service-4里面取出来即可。
这个就是第二个作用,起到上下文的作用UserContextHolder,避免了传参。
ThreadLocal的存储位置
首先来看下Thread、 ThreadLocal 及 ThreadLocalMap 三者存储的位置。
在Thread类里面有个ThreadLocalMap变量,如下图,因为存在线程里面,这样才能做到线程独有。
在ThreadLocalMap里面有很多个Entry,这个Entry的key就是弱引用的threadlocal,value就是需要存储的值。
为什么在ThreadLocalMap里会有多个Entry呢,因为我们在使用的时候可以定义多个ThreadLocal,而这些值最终的存储就是一个一个的Entry。
有了上面宏观上的感受,我们再来看下源码分析,首先看set方法:
1 | java复制代码public void set(T value) { |
如果map为空的话先进行创建
初始化的过程也比较简单,新创建一个数组,根据hash值计算位置,然后把key和value放到该位置上
1 | java复制代码ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
我们再看下map.set方法,set的时候也是先计算位置,如果位置上已经有值的,就是我之前这个key,则把value的值进行替换,如果是null则执行replaceStaleEntry方法,否则的话就移动到下一个位置。
1 | java复制代码private void set(ThreadLocal<?> key, Object value) { |
我们知道Hashmap当发生冲突的时候,采用的是拉链法(也叫链地址法),而我们这的ThreadLocalMap采用的是线性探测法,如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。感兴趣的小伙伴可以看下《ConcurrentHashMap源码精讲》 。
我们再来看下get方法,这个方法也很简单,先从线程中拿到ThreadLocalMap,然后再从map中传入this自己作为key,来拿到Entry,再从Entry中拿到value。
1 | java复制代码public T get() { |
如果map为空的话则进行初始化操作setInitialValue,这个跟上面的set方法里面的逻辑是一样的。
1 | java复制代码private T setInitialValue() { |
强引用和弱引用
标题中已经提到了强引用和弱引用,还有上面讲到的Entry里面的key是ThreadLocal的弱引用,那么具体什么是强引用,什么是弱引用,这里做下介绍。
先看下强引用的代码:
1 | java复制代码public class ReferenceExample { |
运行一下,没有被回收掉
我画了个示意图大家看下,一开始object和strongRef都指向了堆区的new Object()对象。
后来执行object = null,相当于栈和堆之间的连线断掉了,所以在System.gc()以后,由于strongRef还连接着new Object(),所以就没有被释放掉。
再看下弱引用的代码:
1 | java复制代码public class ReferenceExample { |
再执行一下,结果为null,已经被回收掉了
弱引用的连接就很弱,这根虚线等于没有,形同虚设,在回收的时候new Object()一看没人在引用了,那么就直接回收掉了,所以打印weakRef的时候就为null。
所以在上面看源码中会出现k==null的判断,就是因为threadlocal是弱引用,当我们在业务代码中执行了 ThreadLocal instance = null 操作,我们想要清理掉这个 ThreadLocal 实例,由于是弱引用,就像上面的例子一样,经过垃圾回收以后key会变为null,那么这个Entry一直在数组里占着是不行的,所以会把key==null的给清理掉。
对于垃圾回收不是很懂的小伙伴可以看下《一篇文章搞懂GC垃圾回收》 。
内存泄露/remove()方法
首先说下用完ThreadLocal一定要调用remove()方法!一定要调用remove()方法!一定要调用remove()方法! 否则就是会造成内存泄露。
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。
Key 的泄漏
在上面提到过key是弱引用,如果是强引用的话,当执行ThreadLocal instance = null的时候,key还在引用着threadlocal,这时候就不会释放内存,那么这个Entry就一直存在数组中,得不到清理,越堆越多。
但是如果采用弱引用,key会变为null,JDK帮我们考虑了这一点,在执行 ThreadLocal 的 get、set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,
这样,value 对象就可以被正常回收了,防止内存泄露。
value的泄露
虽然解决了key的泄露,但是我们知道value是强引用,我们看下下面的调用链:
Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。
这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,而ThreadLocal的get、set、remove、rehash 方法也没有被调用的话,那么这个value指向的内存也一直存在,一直占着。解决这种情况,就是使用remove方法。看下源码:
1 | java复制代码public void remove() { |
还有一种危险,如果线程是线程池的话,在线程执行完代码的时候并没有结束,只是归还给线程池,那么这个线程中的value就一直被占着,得不到回收,造成内存泄露。所以我们在编码中要养成良好的习惯,不再使用ThreadLocal的时候就要调用remove()方法,及时释放内存。最后感谢大家的收看~
本文转载自: 掘金