threadlocal存连接对象的目的_通过8个问题全面理解 JAVA 并发编程工具ThreadLocal

一、概述

ThreadLocal类是作为线程内部的局部变量而提供的。让这些变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。

通常我们因为程序实现的必要而创建的成员变量大多都是不安全的,因为这些变量被所有的线程共享,由于java内存可见性问题,不同线程对这些共享变量修改后不可见而导致线程安全问题。

通过ThreadLocal创建的变量只能被当前线程访问,对其他线程不可见,故别的线程无法访问和修改,也就是说:对线程公有化变成对线程私有化。事实上每个线程中都有一个ThreadLocal变量副本。

二、应用场景

每个线程都需要一个独享的对象,比如对时间的处理类SimpleDateFormat,如果我们每次使用都new一个对象,很显然这非常的浪费系统性能,同时如果直接作为成员变量,这会被所有线程共享,这又导致线程不安全,要解决这个问题我们可以使用ThreadLocal管理起来就完美了。比如:

可能有的读者朋友已经发现,这样做还不是每个线程创建一个SimpleDateFormat对象吗?这跟直接在方法中new一个对象没有区别啊!然而事实上真的是这样的吗?我么知道1个请求进来就是一个线程,假设有N个线程都在使用dateToStr()方法,如果是直接new的话会产生三个SimpleDateFormat对象,然而用ThreadLocal只会创建一个对象,每一个线程获取一个ThreadLocal对象副本,一个线程一个,这样就实现了同一个变量对线程的私有化。

每个线程内需要保存全局变量,这在一定层级上减少代码耦合度。以上列举为ThreadLocal常见应用场景,而在实际的代码开发中我们还有根据需要进一步的进行细化。

三、核心知识

1、类关系

每个Thread线程对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap内部又维护由Entry组成的N个节点,这 N个节点也就是Entry数组,每个Entry代表一个完整的对象,Entry对象维护了一堆 k-v对,key是ThreadLocal本身,value是ThreadLocal的泛型值,也就是ThreadLocal的副本。

核心源码如下

2、类关系图

ThreadLocal内存结构图。

3、主要方法

initialValue():初始化,会在get()方法里进行懒加载的。get():得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue方法进行初始化。我们看下 get()方法的源码

set():为这个线程设置一个新值。源码如下:

remove():删除这个线程对应的值,防止内存泄露的最佳手段,这也是《阿里巴巴 JAVA 开发规范》中为什么强调至少调用一次 remove()方法的原因。源码如下:

3.1、initialValue

3.1.1、什么意思

见名知意,初始化一些value(泛型值),就是上文中说的 Entry 对象的 value 值,即ThreadLocal的泛型值,懒加载的。

3.1.2、触发时机

通过上文的描述和对源码的解读可知,该方法是在 get()方法中被触发的。如果在 get()被调用前没有调用过 set()方法,这时候就会在 get()方法内部触发initialValue,通俗讲就是在调用 get 方法的时候没有拿到自己想要的东西,则会触发initialValue。

3.1.3、补充说明

通常,每个线程最多调用一次此方法。但是如果已经调用了remove(),然后再次调用get()的话,则可以再次触发initialValue。如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。比如:

3.1.4、源码

3.2、get

3.2.1、什么意思

获取当前线程下的ThreadLocal中的值。

3.2.2、源码

3.3、set

3.3.1、什么意思

其实干的事和initialValue是一样的,都是set值,只是调用时机不同。set是想用就用,api摆在这里,你想用就调一下set方法。很自由。

3.3.2、源码

3.4、remove

3.4.1、什么意思

将当前线程下的ThreadLocal的值删除,目的是为了减少内存占用。主要目的是防止内存泄漏。内存泄漏问题下面会说。

3.4.2、源码

4、ThreadLocalMap

为啥单独拿出来说下,我就是想强调一点:这个东西是归Thread类所有的。它的引用在Thread类里,这也证实了一个问题:ThreadLocalMap类内部为什么有Entry数组,而不是Entry对象?

因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为再说一次,ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

核心源码如下:

四、完整源码

1、核心源码

2、set()

3、getMap()

4、map.set()

5、createMap()

6、get()

7、setInitialValue()

8、initialValue()

9、remove()

10、小结

其实什么东西都怕用心的专研,只要我们理清楚了相关的类关系,ThreadLocal也就没有那么的神秘莫测了。Thread、ThreadLocal、ThreadLocalMap、Entry一句话总结就是:Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。

五、答疑(面试题)

1、和Synchronized的区别

问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。

不是这样的。

同步机制保证:多线程同时操作共享变量并且能正确的输出结果。ThreadLocal把共享变量变成线程私有了,每个线程都有独立的一个变量。

2、存储在jvm的哪个区域

问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?

不是这样的,还是在堆内存中。ThreadLocal对象也是对象,是对象就在堆中。只是JVM通过一些技巧将其可见性变成了线程可见。

3、真的只是当前线程可见吗?

问:真的只是当前线程可见吗?

好像不是这样的,通过这个InheritableThreadLocal类就可以实现多个线程访问ThreadLocal的值,但是我确实没有去研究过,广大同行小伙伴如果有人知道,欢迎在评论区留言分享,先谢过啦!

4、会导致内存泄漏吗?

问:会导致内存泄漏么?

首先我们先对这个问题进行2点分析:

1、ThreadLocalMap.Entry的key会内存泄漏吗?2、ThreadLocalMap.Entry的value会内存泄漏吗?话不多说,先看key-value的核心源码:

从继承关系上来看,是继承的弱引用,key是直接交给父类super(key)来处理的,父类就是一个弱引用,因为弱引用会被GC回收的,故此key完全不存在内存泄露问题。

弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。

由源码可知这里的value是一个强引用,这是不是可能会导致内存泄露呢?

但是我们仔细琢磨一下,这里线程都终止了,不管是强引用还是弱引用都必然会被GC回收的,很简单,因为引用链断掉了。

这样看起来似乎没有问题,但是我们这里忘记了一个重要角色:线程池,在线程池核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:

Thread->ThreadLocalMap->Entry(key为null)->value

由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,内存泄漏(OOM)也是迟早的事情。

那怎么办,这么说来ThreadLocal也是不能使用的了?非也。其实作者在开发ThreadLocal的时候就已经想到并给出了解决方案:

ThreadLocal提供了remove()方法,它的作用就是将value移出去的,所以用完后记得调用remove()方法释放资源哟。

5、为什么用Entry数组而不是Entry对象

这个其实主要想考ThreadLocalMap是在Thread里持有的引用。

问:ThreadLocalMap内部的table为什么是数组而不是单个对象呢?

这个其实也很好理解,在实际开发中有时候根据业务需要会new出来好几个ThreadLocal对象,各自承担一份工作。在一个线程里面ThreadLocalMap是同一个,而不是多个,不管new了多少个ThreadLocal对象,为什么呢?因为ThreadLocalMap的引用是在Thread里面的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

6、你学习的开源框架哪些用到了ThreadLocal

Spring框架。

DateTimeContextHolder

RequestContextHolder

7、ThreadLocal里的对象一定是线程安全的吗

不一定。

假设ThreadLocal.set()进去的值本身就是一个共享变量,如static对象,那么我们在get()的时候获取的还是这个共享对象本身,还是避免不了线程安全问题。

所以大家在使用的时候也一定谨慎!

8、笔试题

问:下面这段程序会输出什么?为什么?

答:

是不是对上面的日志输出信息感到疑惑,为什么输出1后就抛出了空指针异常?

这里输出1是完全没有问题的,但是主线程抛出了空指针这到底是怎么一回事呢?

假设你是如下回答:

要是真是这个回答,那我只能说你根本连ThreadLocal是啥都不知道。非常明显这是子线程和主线程两个线程,子线程中设置为1,根据线程的特点(如果不知道,自行百度,这里不在赘述)主线程肯定拿不到子线程中的数据,上文中的介绍清楚的说明了,ThreadLocal和线程是息息相关的。

那么为什么会抛出空指针?

因为你的get()方法返回的是long而不是Long,根据Java中基础类型和包装类型的知识:long是基本类型,默认值是0L,Long是包装类型,默认是null,ThreadLocal的get()返回null,long自然不能接受null值,故此空指针。

ThreadLocal里的泛型是Long,get却是基本类型,这需要拆箱操作的,也就是会执行null.longValue()的操作,这自然就会空指针了。

看似一道Javase的基础题目,实则隐藏了很多知识。

六、ThreadLocal工具类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值