一、ThreadLocal
1、概要:
- JDK1.2开始支持java.lang.Threadlocal
- 说明:一些变量只有当前线程可以访问,每个线程都有自己的变量副本。线程消亡他也消亡,他是变量对象,不是线程
- 他可以在一些情况下代替多线程和线程同步机制
- 使用案例:创建static的threadlocal变量,这样调用其他方法时就可以不用传某个参数了,直接去threadlocal中获取即可,可以保证线程安全
- 如何做到的?:Thread类中有一个属性
ThreadLocal.ThreadLocalMap
,该属性key为每个threadlocal变量,value为该变量的值(当前线程)- 老版本的解决方案:每个threadlocal变量有个map,key为线程id,value为线程的值
![img](https://i-blog.csdnimg.cn/blog_migrate/a88fc3cc0777c3b84516e01f783c396e.png)
2、ThreadLocal接口方法
// 创建案例
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
ThreadLoca类只有4个方法,支持泛型
void set(T value)
设置当前线程的threadlocal的值public T get()
该方法返回当前线程threadlocal的值public void removed()
将当前线程threadlocal的值删除。
目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度protected T initialValue()
返回该线程threadlocal的初始值,该方法是一个 protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用geto或 set(Object))时才执行,并且仅执行1次。 ThreadLocal中的默认实现直接返回一个null
创建threadlocal的同时指定初始值:
private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {//为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。
return 0;
}
};
3、原理概览
public class Thread implements Runnable {
// thread内部成员,每个线程都有一个map,s代表有可以保存多个threadLocal
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap是ThreadLocal的一个内部类,其作用相当于一个HashMap,用于保存隶属于该线程的变量副本。
}
虽然Thread中的ThreadLocalMap属性为空,但是并不需要我们手动创建赋值,因为set()或get()
的时候会自动创建map
![img](https://i-blog.csdnimg.cn/blog_migrate/c34c3b8ba6e65c0fb9916187d48407ec.jpeg)
4、使用案例
案例1:threadlocal用作数据库连接
根本原因是数据库连接必须一个线程使用一个,防止线程不安全
我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能
案例2:SimpleDateFormat
首先介绍SimpleDateFormat是什么,就是输出指定格式的日期
主要方法:
-
//创建: SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 把Date型的字符串 转换成 特定格式的String类型 String str = sdf.format(new Date()); // 2015-03-27 21:13:23 //方法可以把String型的字符串 转换成 特定格式的date类型 Date date = sdf.parse("2015-03-27 21:13:23");
线程不安全:
该方法线程不安全,线程不安全的原因:如果该对象被多个线程操作,那么多个线程都是操作的其中的calendar属性
// 源码
protected Calendar calendar;
Date parse() {
calendar.clear();
...; // 执行一些操作, 设置 calendar 的日期什么的
calendar.getTime(); // 获取calendar的时间
}
SimpleDateFormat线程不安全的解决方案:
- 使用同步(不推荐)
- 使用
ThreadLocal<SimpleDateFormat>
- 使用线程安全的类:Instant 代替 Date, LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat
/**
https://blog.csdn.net/weixin_42456466/article/details/80757114
* 日期工具类(使用了ThreadLocal获取SimpleDateFormat,其他方法可以直接拷贝common-lang)
*/
public class DateUtil {
private static Map<String,ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();
private static Logger logger = LoggerFactory.getLogger(DateUtil.class);
public final static String MDHMSS = "MMddHHmmssSSS";
public final static String YMDHMS = "yyyyMMddHHmmss";
public final static String YMDHMS_ = "yyyy-MM-dd HH:mm:ss";
public final static String YMD = "yyyyMMdd";
public final static String YMD_ = "yyyy-MM-dd";
public final static String HMS = "HHmmss";
/**
* 根据map中的key得到对应线程的sdf实例
* @param pattern map中的key
* @return 该实例
*/
private static SimpleDateFormat getSdf(final String pattern){
ThreadLocal<SimpleDateFormat> sdfThread = sdfMap.get(pattern);// 获取指定日期格式的 当前线程的 SimpleDateFormat
if (sdfThread == null){
//双重检验,防止sdfMap被多次put进去值,和双重锁单例原因是一样的
synchronized (DateUtil.class){
sdfThread = sdfMap.get(pattern);
if (sdfThread == null){
logger.debug("put new sdf of pattern " + pattern + " to map");
sdfThread = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
logger.debug("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern,sdfThread);
}
}
}
return sdfThread.get();
}
/**
* 按照指定pattern解析日期
* @param date 要解析的date
* @param pattern 指定格式
* @return 解析后date实例
*/
public static Date parseDate(String date,String pattern){
if(date == null) {
throw new IllegalArgumentException("The date must not be null");
}
try {
return getSdf(pattern).parse(date);//返回Date
} catch (ParseException e) {
e.printStackTrace();
logger.error("解析的格式不支持:"+pattern);
}
return null;
}
/**
* 按照指定pattern格式化日期
* @param date 要格式化的date
* @param pattern 指定格式
* @return 解析后格式
*/
public static String formatDate(Date date,String pattern){
if (date == null){
throw new IllegalArgumentException("The date must not be null");
}else {
return getSdf(pattern).format(date);
}
}
}
4、TreadLocal优势:
ThreadLocal与 Thread同步机制的比较
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间 的方式,只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用以空间换时间 的方式, 为每一个线程都提供了一份变量的副本, 从而实现同访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
为什么不适用session代替threadlocal
我们既然可以使用httpsession,为什么还要多此一举考虑ThreadLocal来管理session?
- 1.HttpSession如果存放了大量的数据,会影响系统性能:
- 2.我们在controller层使用HttpSession比较方便,当我们想在service层或者dao层使用时,就比较麻烦了,需要从controller传值
但是要注意tomcat线程复用的问题,解决方案:
- 1、保证每次都用新的值覆盖线程变量;
- 2、保证在每个请求结束后清空线程变量。
5、thread、threadlocal、map关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Orzpt25o-1615561115811)(https://i-blog.csdnimg.cn/blog_migrate/c34c3b8ba6e65c0fb9916187d48407ec.jpeg)]
上面这张图详细的揭示了ThreadLocal
和Thread
以及ThreadLocalMap
三者的关系。
1、Thread中有一个map,就是ThreadLocalMap(map里只有一个Entry?不是的,可能有很多Entry,但与当前计算的threadlocal相关的entry只有一个)
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,内存不够时,会被当成垃圾回收,变为null
4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
- 每个线程里有个threadLocals变量,我们把ThreadLocalThread赋给threadLocals
二、源码
1、源码-属性
下面是ThreadLocal的类图结构,
![img](https://i-blog.csdnimg.cn/blog_migrate/95f80a93098b73d8365dc327f9613e31.png)
从图中可知:Thread类中有两个变量
Thread.ThreadLocalMap threadLocals=null
Thread.ThreadLocalMap inheritableThreadLocals=null
说明 :
- 为什么为空:由上面我们知道初始时两个map为空,那么为什么如需程序员自己创建map呢?因为调用set()或get()时会先确保但当前线程的map已经被创建。也就是懒加载
- 缺点:线程不终止,map和值就还存在,所以可以调用remove方法手动提前删除threadLocals本地变量
- 两个map的区别:类型一样
2、源码-方法
推荐阅读:https://www.cnblogs.com/fsmly/p/11020641.html
2.1、set()
顺序threadLocal的set方法:
// threadLocal.set
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);//getMap直接返回t.threadLocals。即线程的属性threadLocals,该属性的类型是ThreadLocal.ThreadLocalMap // 每个线程的该属性初始值为null
//(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else // 创建同时添加
createMap(t, value);
}
而map的set逻辑和hashMap的put差不多,都是按i进行哈希计算
不同点是:
- i发生碰撞的话就后移一个元素位置放下
- 如果该位置的key为空或者跟要放入的一致才放入成功
// ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 获取索引值
int i = key.threadLocalHashCode & (len-1);
//遍历tab如果已经存在则更新值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//return ((i + 1 < len) ? i + 1 : 0); //ThreadLocal解决哈希冲突居然用的是i+1存储
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 走到这说明k!=key
if (k == null) { // 如果key为null的话,擦除
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);//以{threadlocal,value}作为键值对形参Entry
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2.2、getMap()、createMap()
getMap获取map(不是获取值)
ThreadLocalMap getMap(Thread t) {
// 每个线程维护的threadLocals
return t.threadLocals;
}
// 第一个存threadlocal的时候创建ThreadLocalMap,这个map与当前线程关联,不需要传入key,因为第一个key是threadlocal
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给当前线程的成员变量threadLocals // 由这个s我们也能体会出每个线程内有多个threadLocal
// 传入的this和firstValue会作为map中的第一个键值对
t.threadLocals = new ThreadLocalMap(this, firstValue);// 创建后放到了Thread的属性中
}
为什么set的时候不需加锁:因为只是操作的当前线程的属性
2.3、get()
思路:
- 拿到当前线程,
- 获取当前线程里的map,即线程类里的threadLocals属性。
- 如果map为空,创建map,同时把默认值传进去作为构造函数(构造完map顺便就把默认值设置进去了)
// 返回当前线程下threadLocalMap里以threadlocal为key的value值。、
// this为threadlocal具体变量
public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
// map中有多个threadlocal键值对,从map中拿到此threadlocal对应的值
ThreadLocalMap.Entry e = map.getEntry(this);//当前索引key可能不等,那就判断下一索引位,因为我们创建时是这样解决哈希冲突的
if (e != null) {
//获取实体e对应的value值,即threadlocal值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
// 执行到这里的两种情况
// 情况1:map不存在
// 情况2:map存在,但没有与当前ThreadLocal关联的entry
return setInitialValue();
}
// 情况1:map不存在
// 情况2:map存在,但没有与当前ThreadLocal关联的entry
private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);// this是当前threadlocal
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}
2.4、remove()
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
// 判断当前线程的ThreadLocalMap为不为空,不为空就主动移除掉map里的当前threadlocal。需要主动调用remove,否则会有内存溢出,即线程只要不消亡,threadlocal就还在,他的value也在
public void remove() {
//获取当前线程绑定的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}
/** ThreadLocalMap类的方法,根据key删除对应的entry */
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// key可能发生过碰撞,所以用nextIndex(i, len)计算下一个i
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
3、ThreadLocalMap内部实现Entry
Thread.ThreadLocalMap内部实际上是一个Entry数组private Entry[] table
,我们先看看Entry的这个内部类
Entry类特点:
- 继承自WeakReference
- 构造参数中value正常赋值给属性,而key是通过父类来存储的
- key的存储方式是弱引用,弱引用的构造函数
(引用对象,引用队列)
。引用队列存放引用被回收的对象 - 如果
entry.get() == null
则代表引用消失了,要把对应的entry 从table中移除
//public class ThreadLocal<T> {
static class {
static class Entry extends WeakReference<ThreadLocal<?>> { // 弱引用,弱引用在GC时一定被释放
/** value就是和ThreadLocal绑定的 */
Object value;
//k:ThreadLocal的引用,被传递给WeakReference的构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
super(referent); //referent:ThreadLocal的引用
}
//Reference构造方法
Reference(T referent) {
this(referent, null);//referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;//引用队列
}
在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。
那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i
4、内存泄露问题
ThreadLocal.ThreadLocalMap,其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。
每个线程的本地变量存放在自己的本地内存变量Map中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
ThreadLocalMap
是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/fd3c665b6fae597d9a456d959275e69f.png)
要搞懂问题需要我们了解的问题有:JVM四种引用、ThreadLocalMap内部构造
- 内存溢出overflow
- 内存泄露leak,无法回收
JVM四种引用:
- ①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
- ②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
- ③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)
threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露.
正确的用法:将调用threadlocal的remove(): 把当前ThreadLocal从当前线程的ThreadLocalMap中移除。(包括key,value)
当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露.
为什么key使用弱引用
https://blog.csdn.net/puppylpg/article/details/80433271
不妨反过来想想,如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用(即:TL_INT
,是一个强引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
那使用弱引用的好处呢?
如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:TL_INT
强引用,和ThreadLocalMap中Entry的弱引用。一旦TL_INT
被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。
那么问题来了,ThreadLocal@123456对象只是作为ThreadLocalMap的一个key而存在的,现在它被回收了,但是它对应的value并没有被回收,内存泄露依然存在!而且key被删了之后,变成了null,value更是无法被访问到了!针对这一问题,ThreadLocalMap类的设计本身已经有了这一问题的解决方案,那就是在每次get()
/set()
/remove()
ThreadLocalMap中的值的时候,会自动清理key为null的value。如此一来,value也能被回收了。
既然对key使用弱引用,能使key自动回收,那为什么不对value使用弱引用?答案显而易见,假设往ThreadLocalMap里存了一个value,gc过后value便消失了,那就无法使用ThreadLocalMap来达到存储全线程变量的效果了。(但是再次访问该key的时候,依然能取到value,此时取得的value是该value的初始值。即在删除之后,如果再次访问,取到null,会重新调用初始化方法。)
强引用能解决内存泄露吗
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄露吗?
- threadlocal是作为key的,是我们自己定义的,也就是说有时候threadlocal变量是不消失的,如静态变量
- threadlocal不消失,那么就永远可以查到。但是不用的时候就threadlocal本身就不会GC了,因为key指向threadlocal
- 如果是弱引用,threadlocal=null后,就是说这个threadlocal永远不用了,但是entry还在,也就是说,entry是线程的里map的问题,不是threadlocal的问题。
- 也就是说,ThreadLocalMap中的key使用了强引用,是无法完成避免内存泄露的
- 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
- 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
- Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作
- 图中的虚线,表示key对ThreadLocal实例的引用是个弱引用
如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
Threadref–>Thread–>ThreadLocalMap–>Entry
(下面那条),所以这将导致内存泄漏。
5、ThreadLocal子线程继承性问题
同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)
public class ThreadLocalTest2 {
//(1)创建ThreadLocal变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}
在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。
6、源码其他内容
- key可能发生过碰撞,所以用nextIndex(i, len)计算下一个i
静态内部类和非静态内部类之间区别:
- 内部静态类不需要有指向外部类的引用。但非静态内部类需要。
- 静态类只能访问外部类的静态成员,非静态内部类能够访问外部类的静态和非静态成员。
- 非静态内部类不能脱离外部类实体被创建,非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面。
public class ThreadLocal<T> {
/**
threadLocalHashCode是当前threadlocal的哈希值,每次调用它的适合都会更新一个新值。
更新方法为从0开始,每获取一次就+0x61c88647。为什么是这个增量值?因为他哈希值平均
*/
private final int threadLocalHashCode = nextHashCode();
// 下一个哈希值,自动更新,从0开始
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。
0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:(16进制取最后一位)
hashCode 数组下标
0x61c88647 7
0xc3910c8e 14
0x255992d5 5
0x8722191c 12
0xe8ea9f63 3
0x4ab325aa 10
0xac7babf1 1
0xe443238 8
0x700cb87f 15
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
常量,在实例化完成之前有值即可
扩展:一个thread内的hashCode是按上面的顺序创建的吗?答案为不是,因为ThreadLocal多线程可以交叉调用
*/
private static final int HASH_INCREMENT = 0x61c88647;
// 获取下一个哈希值
private static int nextHashCode() {
// nextHashCode是AtomicInteger类型的,保证了
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 当前线程的threadlocalMap中key为threadlocal的默认值,这里默认值为null // 如果想改变默认值,我们可以继承类后重写该方法
protected T initialValue() {
return null;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
// 空构造
public ThreadLocal() {}
/**
* Factory method to create map of inherited thread locals.
* Designed to be called only from Thread constructor.
*
* @param parentMap 父线程关联的map
* @return a map containing the parent's inheritable bindings
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
/**
* Method childValue is visibly defined in subclass
* InheritableThreadLocal, but is internally defined here for the
* sake of providing createInheritedMap factory method without
* needing to subclass the map class in InheritableThreadLocal.
* This technique is preferable to the alternative of embedding
* instanceof tests in methods.
*/
T childValue(T parentValue) {
throw new UnsupportedOperationException();
}
//------------分割线------------------
/**
* An extension of ThreadLocal that obtains its initial value from
* the specified {@code Supplier}.
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
//------------分割线------------------
/**
ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。
ThreadLocalMap是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.
To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
//只有内部类可以为static
static class ThreadLocalMap {
// 懒加载的初始容量,编译期就确定的常量
private static final int INITIAL_CAPACITY = 16;
// 这个数组的容量也必须是2的幂
private Entry[] table;
// table内entries的数量
private int size = 0;
// 扩容阈值,跟hashmap的阈值同理
private int threshold; // Default to 0
//设置阈值为2/3容量,len为容量
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 获取下一个坐标
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 获取上一个索引
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
// 当当前线程第一个调用threadlocal的方法时,创建map,且把传入的值作为第一个threadlocal键值对 // map是懒加载的
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//
//初始容量为16
table = new Entry[INITIAL_CAPACITY];
//位运算,计算出需要存放的位置table[i] // 第一个并不是存在table[0]
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
// 走到这是因为get的时候索引位置没有entry,或者虽然有entry但是threadlocal不对
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);//i+1继续判断
e = tab[i];
}
return null;
}
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
// 扩容table容量×2
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 获取新的索引位置,原来的位置或者+oldLen的位置
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
// 如果当前位置有值了就存在下一索引位置
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
}
}
子线程访问不了主线程的threadlocal
同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)
public class ThreadLocalTest2 {
//(1)创建ThreadLocal变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}
InheritableThreadLocal
在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码
// 注意操作的是inheritableThreadLocals属性即可
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
思路整理:
-
每个线程有每个线程的ThreadLocalMap,
-
map 的key是threadLocal值,
-
map的value是Entry类型,而Entry.value是真正的值
-
set方法:先根据当前线程找到当前线程的ThreadLocalMap,
-
如果有map:把threadLocal值作为key更新值。如果地址冲突后索引值+1
-
如果没有map:
new ThreadLocalMap(ThreadLocal变量, value);
-
Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value(get 方法会在遍历的时候如果遇到key为null,就调用expungeStaleEntry方法擦除,set方法在遍历的时候,如果遇到key为null,就调用replaceStaleEntry方法替换掉。见下面代码)。
所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。
-
-
get方法:先获取map,然后从map中获取
ThreadLocal变量
对应的值Entry- 获取到后然后
entry.value
- 获取不到设置entry为null//设置为null,我们可以创建子类重写初始值
- 获取到后然后
-
remove方法:获取map后移除threadLocal值