深刻解析ThreadLocal 详解、实现原理、使用场景方法以及内存泄漏防范 多线程中篇(十七)

简介

从名称看,ThreadLocal 也就是thread和local的组合,也就是一个thread有一个local的变量副本
ThreadLocal提供了线程的本地副本,也就是说每一个线程将会拥有一个本身独立的变量副本
方法简洁干练,类信息以及方法列表以下
image_5c64cced_8d

示例

在测试类中定义了一个ThreadLocal变量,用于保存String类型数据
建立了两个线程,分别设置值,读取值,移除后再次读取
package test2;
/**
* Created by noteless on 2019/1/30. Description:
*/
public class T21 {
//定义ThreadLocal变量
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
//thread1中设置值
threadLocal.set("this is thread1's local");
//获取值
System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
//移除值
threadLocal.remove();
//再次获取
System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
}, "thread1");
Thread thread2 = new Thread(() -> {
//thread2中设置值
threadLocal.set("this is thread2's local");
//获取值
System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
//移除值
threadLocal.remove();
//再次获取
System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
}, "thread2");
thread1.start();
thread2.start();
}
}
执行结果
image_5c64cced_360f
从结果能够看获得,每一个线程中能够有本身独有的一份数据,互相没有影响
remove以后,数据被清空
 
从上面示例也能够看出来一个状况:
若是两个线程同时对一个变量进行操做,互相之间是没有影响的,换句话说,这很显然并非用来解决共享变量的一些并发问题,好比多线程的协做
由于ThreadLocal的设计理念就是共享变私有,都已经私有了,还谈啥共享?
好比以前的消息队列,生产者消费者的示例中
  final LinkedList<Message> messageQueue = new LinkedList<>();
若是这个LinkedList是ThreadLocal的,生产者使用一个,消费者使用一个,还协做什么呢?
可是共享变私有,如同并发变串行,或许适合解决一些场景的线程安全问题,由于看起来就如同没有共享变量了,不共享即安全,可是他并非为了解决线程安全问题而存在的

实现分析

在Thread中有一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap
image_5c64cced_790c
而ThreadLocalMap则是ThreadLocal的静态内部类,他是一个设计用来保存thread local 变量的自定义的hash map
全部的操做方法都是私有的,也就是不对外暴露任何操做方法,也就是只能在ThreadLocal中使用了
此处咱们不深刻,就简单理解为是一个hash map,用于保存键值对
image_5c64ccee_227c
也就是说Thread中有一个“hashMap”能够用来保存键值对

set方法

看一下ThreadLocal的set方法
image_5c64ccee_3823
在这个方法中,接受参数,类型为T的value
首先获取当前线程,而后调用getMap(t)
这个方法也很简单,就是直接返回Thread内部的那个“hashMap”(threadLocals是默认的访问权限)
image_5c64ccee_45bc
继续回到set方法,若是这个map不为空,那么以this为key,value为值,也就是ThreadLocal变量做为key
若是map为空,那么进行给这个线程建立一个map ,而且将第一组值设置进去,key仍旧是这个ThreadLocal变量
image_5c64ccee_63f9
简言之:
调用一个ThreadLocal的set方法,会将:以这个ThreadLocal类型的变量为key,参数为value的这一个键值对,保存在Thread内部的一个“hashMap”中

get方法

在get方法内部仍旧是获取当前线程的内部的这个“hashMap”,而后以当前对象this(ThreadLocal)做为key,进行值的获取
image_5c64ccee_5c16
咱们对这两个方法换一个思路理解:
每一个线程可能运行过程当中,可能会操做不少的ThreadLocal变量,怎么区分各自?
直观的理解就是,咱们想要获取某个线程的某个ThreadLocal变量的值
一个很好的解决方法就是借助于Map进行保存,ThreadLocal变量做为key,local值做为value
假设这个map名为:threadLocalsMap,能够提供setter和getter方法进行设置和读取,内部为
  • threadLocalsMap.set(ThreadLocal key,T value)
  • threadLocalsMap.get(ThreadLocal key)
这样就是能够达到thread --- local的效果,可是是否存在一些使用不便?咱们内部定义的是ThreadLocal变量,可是只是用来做为key的?是否直接经过ThreadLocal进行值的获取更加方便呢?
怎么可以作到数据读取的倒置?由于毕竟值的确是保存在Thread中的
其实也很简单,只须要内部进行转换就行了,对于下面两个方法,咱们都须要 ThreadLocal key
threadLocalsMap.set(ThreadLocal key,T value)
threadLocalsMap.get(ThreadLocal key) 
而这个key不就是这个ThreadLocal,不就是this 么
因此:
  • ThreadLocal.set(T value)就内部调用threadLocalsMap.set(this,T value)
  • ThreadLocal.get()就内部调用threadLocalsMap.get(this) 
因此总结下就是:
  • 每一个Thread内部保存了一个"hashMap",key为ThreadLocal,这个线程操做了多少个ThreadLocal,就有多少个key
  • 你想获取一个ThreadLocal变量的值,就是ThreadLocal.get(),内部就是hashMap.get(this);
  • 你想设置一个ThreadLocal变量的值,就是ThreadLocal.set(T value),内部就是hashMap.set(this,value);
关键只是在于内部的这个“hashMap”,ThreadLocal只是读写倒置的“壳”,能够更简洁易用的经过这个壳进行变量的读写
“倒置”的纽带,就是getMap(t)方法

remove方法

image_5c64ccee_4373
remove方法也是简单,当前线程,获取当前线程的hashMap,remove

初始值

再次回头看下get方法,若是第一次调用时,指定线程并无threadLocals,或者根本都没有进行过set
会发生什么?
以下图所示,会调用setInitialValue方法
image_5c64ccee_5b3
在setInitialValue方法中,会调用initialValue方法获取初始值,若是该线程没有threadLocals那么会建立,若是有,会使用这个初始值构造这个ThreadLocal的键值对
简单说,若是没有set过(或者压根内部的这个threadLocals就是null的),那么她返回的值就是初始值
image_5c64ccee_5f9f
这个内部的initialValue方法默认的返回null,因此一个ThreadLocal若是没有进行set操做,那么初始值为null
image_5c64ccee_12c6
如何进行初始值的设定?
能够看得出来,这是一个protected方法,因此返回一个覆盖了这个方法的子类不就行了?在子类中实现初始值的设置
在ThreadLocal中提供了一个内部类SuppliedThreadLocal,这个内部类接受一个函数式接口Supplier做为参数,经过Supplier的get方法获取初始值
image_5c64ccee_dbd
Supplier是一个典型的内置函数式接口,无入参,返回类型T,既然是函数式接口也就是能够直接使用Lambda表达式构造初始值了!!!
image_5c64ccee_e57
如何构造这个内部类,而后进而进行初始化参数的设置呢?
提供了withInitial方法,这个方法的参数就是Supplier类型,能够看到,这个方法将入参,透传给SuppliedThreadLocal的构造方法,直接返回一个SuppliedThreadLocal
换句话说,咱们不是但愿可以借助于ThreadLocal的子类,覆盖initialValue()方法,提供初始值吗?
这个withInitial就是可以达成目标的一个方法!
image_5c64ccee_73d2
使用withInitial方法,建立具备初始值的ThreadLocal类型的变量,从结果能够看得出来,咱们没有任何的设置,能够获取到值
image_5c64ccee_1240
稍做改动,增长了一次set和remove,从打印结果看得出来,set后,使用的值就是咱们新设置的
而一旦remove以后,那么仍旧会使用初始值
image_5c64ccee_47c5
注意:
对于initialValue方法的覆盖,其实即便没有提供这个子类以及这个方法也都是能够的,由于本质是要返回一个子类,而且覆盖了这个方法
咱们能够本身作,也能够直接匿名类,以下所示:建立了一个ThreadLocal的子类,覆盖了initialValue方法

ThreadLocal <类型 > threadLocalHolder =new ThreadLocal <类型> () { html

public 类型 initialValue() { 浏览器

return XXX; 安全

} session

};多线程

可是很显然,提供了子类和方法以后,咱们就能够借助于Lambda表达式进行操做,更加简介

总结:

经过set方法能够进行值的设定
经过get方法能够进行值的读取,若是没有进行过设置,那么将会返回null;若是使用了withInitial方法提供了初始值,将会返回初始值
经过remove方法将会移除对该值的写入,再次调用get方法,若是使用了withInitial方法提供了初始值,将会返回初始值,不然返回null
对于get方法,很显然若是没有提供初始值,返回值为null,在使用时是须要注意不要引发NPE异常
 
ThreadLocal,thread  local,每一个线程一份,究竟是什么意思?
他的意思是对于一个ThreadLocal类型变量,每一个线程有一个对应的值,这个值的名字就是ThreadLocal类型变量的名字,值是咱们set进去的变量
可是若是set设置的是共享变量,那么ThreadLocal其实本质上仍是同一个对象不是么?
这句话若是有疑问的话,能够这么理解
对于同一个ThreadLocal变量a,每一个线程有一个map,map中都有一个键值对,key为a,值为你保存的值
可是这个值,到底每一个线程都是全新的?仍是使用的同一个?这是你本身的问题了!!!
ThreadLocal能够作到每一个线程有一个独立的一份值,可是你非得使用共享变量将他们设置成一个,那ThreadLocal是不会保障的
这就比如一个对象,有不少引用指向他,每一个线程有一个独立的引用,可是对象根本仍是只有一个
因此,从这个角度更容易理解,为何说ThreadLocal并非为了解决线程安全问题而设计的,由于他并不会为线程安全作什么保障,他的能力是持有多个引用,这多个引用是否能保障是多个不一样的对象,你来决策
因此咱们最开始说的,ThreadLocal会为每一个线程建立一个变量副本的说法是不严谨的
是他有这个能力作到这件事情,可是究竟是什么对象,仍是要看你set的是什么,set自己不会对你的值进行干涉
不过咱们一般就是在合适的场景下经过new对象建立,该对象在线程内使用,也不须要被别的线程访问
以下图所示,你放进去的是一个共享变量,他们就是同一个对象
image_5c64ccef_3c7b

应用场景

前面说过,对于以前生产者消费者的示例中,就不适合使用ThreadLocal,由于问题模型就是要多线程之间协做,而不是为了线程安全就将共享变量私有化
好比,银行帐户的存款和取款,若是借助于ThreadLocal建立了两个帐户对象,就会有问题的,初始值500,明明又存进来1000块,可支配的总额仍是500
那ThreadLocal适合什么场景呢?
既然是每一个线程一个,天然是适合那种但愿每一个线程拥有一个的那种场景(好像是废话)
一个线程中一个,也就是线程隔离,既然是一个线程一个,那么同一个线程中调用的方法也就是共享了,因此说,有时,ThreadLocal会被用来做为参数传递的工具
由于它可以保障同一个线程中的值是惟一的,那么他就共享于全部的方法中,对于全部的方法来讲,至关于一个全局变量了!
因此能够用来同一个线程内全局参数传递
不过要慎用,由于“全局变量”的使用对于维护性、易读性都是挑战,尤为是ThreadLocal这种线程隔离,可是方法共享的“全局变量”
如何保障必然是独立的一个私有变量?
对于ThreadLocal无初始化设置的变量,返回值为null
因此能够进行判断,若是返回值为null,能够进行对象的建立,这样就能够保障每一个线程有一个独立的,惟一的,特有的变量

示例

对于JavaWeb项目,你们都了解过Session
ps:此处不对session展开介绍,打开浏览器输入网址,这就会创建一个session,关闭浏览器,session就失效了
在这一个时间段内,一个用户的多个请求中,共享同一个session
Session 保存了不少信息,有的须要经过 Session 获取信息,有些又须要修改 Session 的信息
每一个线程须要独立的session,并且不少地方都须要操做 Session,存在多方法共享 Session 的需求,因此session对象须要在多个方法中共享
若是不使用 ThreadLocal,能够在每一个线程内建立一个 Session对象,而后在多个方法中将他做为参数进行传递
很显然,若是每次都显式的传递参数,繁琐易错
这种场景就适合使用ThreadLocal
 
下面的示例就模拟了多方法共享同一个session,可是线程间session隔离的示例
public class T24 {
/**
* session变量定义
*/
static ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
/**
* 获取session
*/
static Session getSession() {
if (null == sessionThreadLocal.get()) {
sessionThreadLocal.set(new Session());
}
return sessionThreadLocal.get();
}
/**
* 移除session
*/
static void closeSession() {
sessionThreadLocal.remove();
}
/**
* 模拟一个调用session的方法
*/
static void fun1(Session session) {
}
/**
* 模拟一个调用session的方法
*/
static void fun2(Session session) {
}
public static void main(String[] args) {
new Thread(() -> {
fun1(getSession());
fun2(getSession());
closeSession();
}).start();
}
/**
* 模拟一个session
*/
static class Session {
}
}

 

因此,ThreadLocal最根本的使用场景应该是: 并发

在每一个线程但愿有一个独有的变量时(这个变量还极可能须要在同一个线程内共享)
避免每一个线程还须要主动地去建立这个对象(若是还须要共享,也一并解决了参数来回传递的问题)
换句话说就是,“如何优雅的解决:线程间隔离与线程内共享的问题”,而不是说用来解决乱七八糟的线程安全问题
因此说若是有些场景你须要线程隔离,那么考虑ThreadLocal,而不是你有了什么线程安全问题须要解决,而后求助于ThreadLocal,这不是一回事
既然可以线程内共享,天然的确是能够用来线程内全局传参,可是不要滥用
再次注意:
ThreadLocal只是具备这样的能力,是你可以作到每一个线程一个独有变量,可是若是你set时,不是传递的new出来的新变量,也就只是理解成“每一个线程不一样的引用”,对象仍是那个对象(有点像参数传递时的值传递,对于对象传递的就是引用)

内存泄漏

ThreadLocal很好地解决了线程数据隔离的问题,可是很显然,也引入了另外一个空间问题
若是线程数量不少,若是ThreadLocal类型的变量不少,将会占用很是大的空间
而对于ThreadLocal自己来讲,他只是做为key,数据并不会存储在它的内部,因此对于ThreadLocal
ThreadLocalMap内部的这个Entity的key是弱引用
image_5c64ccef_779
以下图所示,实线表示强引用,虚线表示弱引用
对于真实的值是保存在Thread里面的ThreadLocal.ThreadLocalMap threadLocals中的
借助于内部的这个map,经过“壳”ThreadLocal变量的get,能够获取到这个map的真正的值,也就是说,当前线程中持有对真实值value的强引用
而对于ThreadLocal变量自己,以下代码所示,栈中的变量与堆空间中的这个对象,也是强引用的
  static ThreadLocal<String> threadLocal = new ThreadLocal<>();
不过对于Entity来讲,key是弱引用
image_5c64ccef_3050
当一系列的执行结束以后,ThreadLocal的强引用也会消亡,也就是堆与栈之间的从ThreadLocal Ref到ThreadLocal的箭头会断开
因为Entity中,对于key是弱引用,因此ThreadLocal变量会被回收(GC时会回收弱引用)
而对于线程来讲,若是迟迟不结束,那么就会一直存在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value的强引用,因此value迟迟得不到回收,就会可能致使内存泄漏 
ThreadLocalMap的设计中已经考虑到这种状况,因此ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里全部key为null的value
以get方法为例
image_5c64ccef_48c7
一旦将value设置为null以后,就斩断了引用于真实内存之间的引用,就可以真正的释放空间,防止内存泄漏
image_5c64ccef_751d
可是这只是一种被动的方式,若是这些方法都没有被调用怎么办?
并且如今对于多线程来讲,都是使用线程池,那个线程极可能是与应用程序共生死的,怎么办?
那你就每次使用完ThreadLocal变量以后,执行remove方法!!!!
从以上分析也看得出来,因为ThreadLocalMap的生命周期跟Thread同样长,因此极可能致使内存泄漏,弱引用是至关于增长了一种防御手段
经过key的弱引用,以及remove方法等内置逻辑,经过合理的处理,减小了内存泄漏的可能,若是不规范,就仍旧会致使内存泄漏

总结

ThreadLocal能够用来优雅的解决线程间隔离的对象,必须主动建立的问题,借助于ThreadLocal无需在线程中显式的建立对象,解决方案很优雅
ThreadLocal中的set方法并不会保障的确是每一个线程会得到不一样的对象,你须要对逻辑进行必定的处理(好比上面的示例中的getSession方法,若是ThreadLocal 变量的get为null,那么new对象)
是否真的可以作到线程隔离,还要看你本身的编码实现,不过若是是共享变量,你还放到ThreadLocal中干吗?
因此一般都是线程独有的对象,经过new建立
相关文章
相关标签/搜索