一针见血理解ThreadLocal类
ThreadLocal类具有两个维度:线程维度和变量维度。扔掉线程维度,保留并放大变量维度,虽然思想片面,但是给人的印象却是极深,才能用之出神入化。
ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。很多开发语言在语言级别都提供这种作用域的变量类型。
根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。
还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。
全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。
ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。
ThreadLocal是跨函数的,但是跨哪些函数呢,由线程来定,更灵活。
问题
- 和Synchronized的区别
- 存储在jvm的哪个区域
- 真的只是当前线程可见吗
- 会导致内存泄漏么
- 为什么用Entry数组而不是Entry对象
- 你学习的开源框架哪些用到了ThreadLocal
- ThreadLocal里的对象一定是线程安全的吗
- 笔试题
概述
官方术语
ThreadLocal类是用来提供线程内部的局部变量。让这些变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。
大白话
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。
应用场景
- 每个线程都需要一个独享的对象(比如工具类,典型的就是
SimpleDateFormat
,每次使用都new一个多浪费性能呀,直接放到成员变量里又是线程不安全,所以把他用ThreadLocal
管理起来就完美了。)
/** |
细心的朋友已经发现了,这也是每个线程都创建一个SimpleDateFormat
,跟直接在方法内部new没区别,错了,大错特错!1个请求进来是一个线程,他可能贯穿了N个方法,你这N个方法假设有3个都在使用dateToStr()
,你直接new的话会产生三个SimpleDateFormat
对象,而用ThreadLocal
的话只会产生一个对象,一个线程一个。
- 每个线程内需要保存全局变量(比如在登录成功后将用户信息存到
ThreadLocal
里,然后当前线程操作的业务逻辑直接get取就完事了,有效的避免的参数来回传递的麻烦之处),一定层级上减少代码耦合度。
核心知识
类关系
每个Thread
对象中都持有一个ThreadLocalMap
的成员变量。每个ThreadLocalMap
内部又维护了N个Entry
节点,也就是Entry
数组,每个Entry
代表一个完整的对象,key是ThreadLocal
本身,value是ThreadLocal
的泛型值。
核心源码如下
// java.lang.Thread类里持有ThreadLocalMap的引用 |
类关系图– ThreadLocal内存结构图。
主要方法
initialValue
:初始化。在get
方法里懒加载的。get
:得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue
方法进行初始化。set
:为这个线程设置一个新值。remove
:删除这个线程对应的值,防止内存泄露的最佳手段。
initialValue
什么意思
见名知意,初始化一些value(泛型值)。懒加载的。
触发时机
调用get
方法之前没有调用set
方法,则get
方法内部会触发initialValue
,也就是说get
的时候如果没拿到东西,则会触发initialValue
。
补充说明
- 通常,每个线程最多调用一次此方法。但是如果已经调用了
remove()
,然后再次调用get()
的话,则可以再次触发initialValue
。 - 如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。
比如:
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() { |
源码
// 由子类提供实现。 |
get
什么意思
获取当前线程下的ThreadLocal中的值。
源码
/** |
getEntry
private Entry getEntry(ThreadLocal<?> key) { |
通过threadLocalHashCode
来获取table[]中存放的Entry对象, 也就是ThreadLocal对象
set
什么意思
其实干的事和initialValue
是一样的,都是set值,只是调用时机不同。set是想用就用,api摆在这里,你想用就调一下set方法。很自由。
源码
/** |
remove
什么意思
将当前线程下的ThreadLocal的值删除,目的是为了减少内存占用。主要目的是防止内存泄漏。内存泄漏问题下面会说。
源码
/** |
ThreadLocalMap
为啥单独拿出来说下,我就是想强调一点:这个东西是归Thread
类所有的。它的引用在Thread
类里,这也证实了一个问题:ThreadLocalMap
类内部为什么有Entry
数组,而不是Entry
对象?
因为你业务代码能new好多个ThreadLocal
对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap
是同一个,而不是多个,不管你new几次ThreadLocal
,ThreadLocalMap
在一个线程里就一个,因为再说一次,ThreadLocalMap
的引用是在Thread
里的,所以它里面的Entry
数组存放的是一个线程里你new出来的多个ThreadLocal
对象。
核心源码如下:
// 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。 |
完整源码
核心源码
// 本地线程。Thread:线程。Local:本地 |
set()
/** |
getMap()
// 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。 |
map.set()
// 不多BB,就和HashMap的set一个道理,只是赋值key,value。 |
createMap()
/** |
get()
/** |
setInitialValue()
// 设置初始值 |
initialValue()
// 由子类提供实现。 |
remove()
/** |
小结
只要捋清楚如下几个类的关系,ThreadLocal
将变得so easy!
Thread`、`ThreadLocal`、`ThreadLocalMap`、`Entry |
一句话总结就是:Thread
维护了ThreadLocalMap
,而ThreadLocalMap
里维护了Entry
,而Entry
里存的是以ThreadLocal
为key,传入的值为value的键值对。
答疑(面试题)
如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?
private final int threadLocalHashCode = nextHashCode(); |
对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
为什么不直接用线程id来作为ThreadLocalMap的key?
这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。
和Synchronized的区别
问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。
答:放屁!同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量count++的时候带上synchronized即可解决。ThreadLocal的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。
存储在jvm的哪个区域
问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?
答:不是。还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。
真的只是当前线程可见吗
问:真的只是当前线程可见吗?
答:貌似不是,貌似通过InheritableThreadLocal
类可以实现多个线程访问ThreadLocal
的值,但是我没研究过,知道这码事就行了。
会导致内存泄漏么
问:会导致内存泄漏么?
答:分析一下:
- 1、
ThreadLocalMap.Entry
的key会内存泄漏吗? - 2、
ThreadLocalMap.Entry
的value会内存泄漏吗?
先看下key-value的核心源码
static class Entry extends WeakReference<ThreadLocal<?>> { |
先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key)
,父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。
弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。
看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。
这么分析一点毛病都没有,但是忘了一个主要的角色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:
Thread->ThreadLocalMap->Entry(key为null)->value |
由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。
解决方案ThreadLocal
已经为我们想好了,提供了remove()
方法,这个方法是将value移出去的。所以用完后记得remove()
。
为什么用Entry数组而不是Entry对象
这个其实主要想考ThreadLocalMap
是在Thread
里持有的引用。
:ThreadLocalMap
内部的table为什么是数组而不是单个对象呢?
答:因为你业务代码能new好多个ThreadLocal
对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap
是同一个,而不是多个,不管你new几次ThreadLocal
,ThreadLocalMap
在一个线程里就一个,因为ThreadLocalMap
的引用是在Thread
里的,所以它里面的Entry
数组存放的是一个线程里你new出来的多个ThreadLocal
对象。
你学习的开源框架哪些用到了ThreadLocal
Spring框架。
DateTimeContextHolder |
ThreadLocal里的对象一定是线程安全的吗
未必,如果在每个线程中ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()
获取的还是这个共享对象本身,还是有并发访问线程不安全问题。
笔试题
问:下面这段程序会输出什么?为什么?
public class TestThreadLocalNpe { |
答:
1 |
为什么?
为什么输出个1,然后空指针了?
首先输出1是没任何问题的,其次主线程空指针是为什么?
如果你这里回答
1 |
那我恭喜你,你连ThreadLocal
都不知道是啥,这明显两个线程,子线程和主线程。子线程设置1,主线程肯定拿不到啊,ThreadLocal
和线程是嘻嘻相关的。这个不多费口舌。
说说为什么是空指针?
因为你get方法用的long而不是Long,那也应该返回null啊,大哥,long是基本类型,默认值是0,没有null这一说法。ThreadLocal
里的泛型是Long,get却是基本类型,这需要拆箱操作的,也就是会执行null.longValue()
的操作,这绝逼空指针了。
看似一道Javase的基础题目,实则隐藏了很多知识。
ThreadLocal工具类
package com.duoku.base.util; |
ThreadLocal的内存泄露问题
ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
ThreadLocalMap设计时的对上面问题的对策:
ThreadLocalMap的getEntry函数的流程大概为:
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
即:
1.使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2.使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
Java中的四种引用类型(强、软、弱、虚)
从Java 1.2开始,JVM开发团队发现,单一的强引用类型,无法很好的管理对象在JVM里面的生命周期,垃圾回收策略过于简单,无法适用绝大多数场景。为了更好的管理对象的内存,更好的进行垃圾回收,JVM团队扩展了引用类型,从最早的强引用类型增加到强、软、弱、虚四个引用类型。
Strong Rerence为JVM内部实现。其他三类引用类型全部继承自Reference父类。如下图所示:
强引用(Strong Reference)
Strong Rerence这个类并不存在,默认的对象都是强引用类型,因为有后来的新引用所衬托,所以才起了个名字叫”强引用”。
强引用使用示例如下所示:
String web = "www.threadlocal.cn"; |
如果JVM垃圾回收器 GC 可达性分析结果为可达,表示引用类型仍然被引用着,这类对象始终不会被垃圾回收器回收,即使JVM发生OOM也不会回收。而如果 GC 的可达性分析结果为不可达,那么在GC时会被回收。
软引用(Soft Reference)
软引用是一种比强引用生命周期稍弱的一种引用类型。在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。所以软引用一般用来实现一些内存敏感的缓存,只要内存空间足够,对象就会保持不被回收掉。
软引用使用示例如下所示:
SoftReference<String> softReference = new SoftReference<String>(new String("www.threadlocal.cn")); |
弱引用(Weak Reference)
弱引用是一种比软引用生命周期更短的引用。它的生命周期很短,不论当前内存是否充足,都只能存活到下一次垃圾收集之前。
WeakReference<String> weakReference = new WeakReference<String>(new String("www.threadlocal.cn")); |
输出结果:
weakReference已经被GC回收
虚引用(PhantomReference)
虚引用与前面的几种都不一样,这种引用类型不会影响对象的生命周期,所持有的引用就跟没持有一样,随时都能被GC回收。
需要注意的是,在使用虚引用时,必须和引用队列关联使用。在对象的垃圾回收过程中,如果GC发现一个对象还存在虚引用,则会把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象内存被回收之前采取必要的行动防止被回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
PhantomReference<String> phantomReference = new PhantomReference<String>(new String("www.threadlocal.cn"), new ReferenceQueue<String>()); |