1.ThreadLocal是什么
2.ThreadLocal能干什么
3.ThreadLocal在生产中的利用
4.ThreadLocal源码剖析
5.ThreadLocal内存泄露问题
6.总结
1.ThreadLocal是什么
在开始理解ThreadLocal是什么之前,咱们能够在java里搜一下这个类,看一下源码的正文:
意思大略就是:
ThreadLocal提供了线程公有的局部变量,这些变量不同于一般变量,因为每一个线程在拜访ThreadLocal实例的时候(通过get或者set办法)都有本人的,独立初始化的正本。
ThreadLocal实例通常是类中的公有动态字段,应用它的目标是心愿将状态(用户id或者事务id)与线程关联起来。
2.ThreadLocal能干什么
可能听了下面的形容,你还是一头雾水。简略地来说,ThreadLocal能办到这种事件:
它实现了每一个线程都有本人的专属本地变量正本(本人用本人的变量不必麻烦他人,不和其他人共享,人各一份)。
次要解决了让每个线程绑定本人的值,通过应用get()和set()办法,获取默认值或者将其值更改为以后线程所存的正本的值从而防止了线程平安问题。
3.ThreadLocal在生产中的利用
通过下面的解说,咱们应该明确了ThreadLocal到底是什么,以及怎么用的一个概念,然而具体还是不会用,以及一时半会儿不会想到适宜的应用场景(有什么场景是须要在多线程操作的时候,人手一份?)
此时咱们举一个例子,便能马上明确它的应用场景:
咱们的切入点便是————SimpleDateFormat
对于SimpleDateFormat,咱们应该都很相熟,咱们通常拿这个工具类来转换日期格局,看起来没什么问题,然而一旦应用不小心会有大问题!
咱们先来看一下SimpleDateFormat的源码正文:
SimpleDateFormat不是线程平安的,举荐为每一个线程创立独立的格局实例。如果多个线程同时拜访一个格局,则它必须放弃内部同步。
假如咱们在工作的时候,常常应用SimpleDateForma,咱们便将它抽出来独立为一个工具类,可能咱们会new 一个SimpleDateForma对象,而后对此对象进行复用,殊不知,这种写法的多线程下的危险性!
public class DateUtils { //先新建一个日期转换类 public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 模仿并发环境下应用SimpleDateFormat的parse办法将字符串转换成Date对象 * * @param stringDate * @return * @throws Exception */ public static Date parseDate(String stringDate) throws Exception { return sdf.parse(stringDate); } public static void main(String[] args) throws Exception { for (int i = 1; i < 30; i++) { new Thread(() -> { try { System.out.println(DateUtils.parseDate("2020-11-11 11:11:11")); } catch (Exception e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } }}
就如上述代码,在多线程并发的状况下,应用同一个工具类,会呈现什么问题呢?咱们来运行一下代码看看:
由此看出,在多线程下对一个日期转换工具类进行操作,很容易出bug起因是:
SimpleDateFormat类外部有一个Calendar对象援用,它用来存储和以后SimpleDateFormat对象相干的日期信息,这就会导致一个问题:
如果多个线程援用这个SimpleDateFormat,那么多个线程之间就会共享这个类,同时也共享这个Calendar援用,那么可能这个线程还没有转化实现日期,这个日期就被替换掉了,呈现各种并发操作的问题。
针对以上这种状况,咱们有以下四个办法能够解决这个问题:
1)将SimpleDateFormat定义成局部变量。
毛病:每调用一次转换方法都会创立一个SimpleDateFormat对象,办法完结后又要作为垃圾回收。
2)加锁。
将这个办法加上synchronized串行化,然而会重大影响吞吐量。
3)应用别的工具类
阿里巴巴开发手册嵩山版就指出:
4)就是咱们明天要介绍的,应用ThreadLocal.
public class DateUtils { //public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); /** * 模仿并发环境下应用SimpleDateFormat的parse办法将字符串转换成Date对象 * * @param stringDate * @return * @throws Exception */ public static Date parseDate(String stringDate) throws Exception { return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(stringDate); } public static void main(String[] args) throws Exception { for (int i = 1; i < 30; i++) { new Thread(() -> { try { System.out.println(DateUtils.parseDate("2020-11-11 11:11:11")); } catch (Exception e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } }}
这样,调用它的每一个线程就都应用了一个工具类的拷贝正本,每个线程一份,就不会呈现这样的问题了。
4.ThreadLocal源码剖析
那么ThreadLocal到底是如何办到,每个Thread人手一份变量拷贝的呢?咱们来进行一下源码剖析:
咱们发现,Thread对象外面有一个ThreadLocal对象中的static类,ThreadLocalMap。
而ThreadLocalMap又是由Entry(ThreadLocal<?> k, Object v) 组成的。
由此咱们能够失去上面这幅图:
threadLocalMap实际上是一个以ThreadLocal为key,Object为value的对象。当咱们为threadLocalMap变量赋值,就是以以后的ThreadLocal值为key,传进来的值为value的一个Etry,并在这个threadLocalMap中寄存。
近似的了解为:
1)每一个Thread都有本人的一个ThreadLocal。
2)每一个Thread都有一个ThreadLocalMap,外面保留了一个键值对,以ThreadLocal为key,以传进来的值为value。
3)每个线程要用到ThreadLocal的时候,用以后的线程去Map外面获取,这样每个线程便领有了本人的独立变量。
4)人手一份,竞争条件被彻底消除,在并发模式下是相对的平安变量。
5.ThreadLocal内存泄露问题
其实ThreadLocal是存在着一些问题的:内存透露问题。
什么是内存透露:不再会被应用的对象或者变量占用的内存不能被回收,就是内存透露。
在解释这个景象之前,咱们先看一下Entry,继承了WeakReference类,弱援用。
对于强脆弱虚援用,咱们在后面这篇博客介绍过。
深刻了解JVM(八)——强脆弱虚援用
强援用:当内存不足,JVM开始垃圾回收,对于强援用的对象,就算呈现了OOM也不会对该对象进行回收。
软援用:内存足够的前提下,不回收该对象。内存不够的前提下,回收该对象
弱援用:不论内存是否够用,只有是弱援用,一律回收。
虚援用:**在这个对象呗收集器回收的时候收到一个零碎告诉或者后序增加进一步的解决,是jvm的技术人员用的,
**
为什么Entry用了弱援用?
每当一个function执行结束之后,栈帧销毁,此时ThreadLocal也就没有了,但此时线程的ThreadLocalMap里的某个entry的key援用还指向这个对象。
弱这个key是强援用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存透露。
弱这个key是弱援用,就能大概率缩小内存透露的问题(还有一个key为null的问题),应用弱援用,就能将key援用指向为null。
由此看出,通过弱援用,的确可能肯定水平解决内存透露问题。
不过gc结束之后,ThreadLocalMap就会呈现key为null的Entry,就没有方法拜访这些key为null的entry的value,如果以后线程迟迟不完结(比方线程池的常驻外围线程)这些key为null的entry就会始终存在一条强援用链,而且value可能会超级大,更进一步地拖垮了服务器的性能。
因而弱援用不能100%保障内存不泄露。咱们要在不应用某个ThreadLocal对象后,手动调用remoev办法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果咱们不手动调用remove办法,那么前面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
上图这个办法就是用来解决null key的内存透露问题的。
此外set()办法:
get()办法:
都做了空key的判断
6.总结
1)ThreadLocal 并不解决线程间共享数据的问题
2)ThreadLocal 实用于变量在线程间隔离且在办法间共享的场景
3)ThreadLocal 通过隐式的在不同县城内创立独立实例正本且防止了实例线程平安的问题
4)每个线程都持有一个专属于本人的Map,并保护了ThreadLocal对象与具体实例的映射。(该map只能被本人拜访,所以没有线程平安问题)
5)ThreadLocalMap的Entry对ThreadLocal的援用为弱援用,防止了ThreadLocal对象无奈被回收的问题。
6)都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个办法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象自身从而避免内存透露,属于平安加固的办法