Java关键字ThreadLocal实践
微信公众号:Java流水账
本号记录国服安琪拉日常编程流水帐,欢迎后台留言
为啥写ThreadLocal
背景:发现很多博客关于ThreadLocal的说明写错了,ThreadLocal不是维护了key为Thread对象的Map,而是Thread对象维护了一个key为ThreadLocal的Map。
下面截取的源码说明了这个问题,如果觉得晦涩,可以先看后面的实例,再回过头来看源码。
//类Thread
public class Thread implements Runnable {
/***Thread类内部维护了一个ThreadLocalMap变量***/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
//类ThreadLocal
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程对象
ThreadLocalMap map = getMap(t); //拿到线程私有的ThreadLocalMap
if (map != null)
map.set(this, value); //ThreadLocal对象为key
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取Thread对象私有ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//拿到节点
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //设置初始值
}
//设置Thread对象的ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //hashcode & 运算去除高位 等同于取余
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
一般像网络请求,使用线程池技术的,Tomcat/Jetty等容器,一个请求一个线程来处理的,如果有一些信息是线程共享的,有二种方式保证线程安全:
-
一种就是常规的加锁,当然也包括cas这种;
-
一种就是使用ThreadLocal做线程隔离,线程访问的是私有的变量,修改的也是线程对象Map自己的那份。
这种思想是典型的用空间换取降低线程安全风险和加锁耗时的做法。
看一个多线程处理请求的例子,这张图可以结合源码看:
在这里插入图片描述Request对象是Handler的成员变量,多线程访问有线程安全问题,使用ThredLocal对Request对象做线程隔离,可以让多线程执行handle()线程安全,再次说明:线程内部Map key为ThreadLocal对象,value为Request对象,因此实际上是每个线程有自己的线程局部变量,省去了锁的开销。
下面是一段简单用法,只是为了演示写的。
public static void main(String[] args) {
ThreadLocal<Person> threadLocal = new ThreadLocal<>(); //每个线程局部变量都需要创建一个ThreadLocal对象
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
AtomicInteger atomicInteger = new AtomicInteger(1000);
ExecutorService executorService = Executors.newCachedThreadPool();
//for(int i 0->10)
executorService.submit(() -> {
Person person = new Person();
person.setIdcard(atomicInteger.getAndIncrement());
person.setName(Thread.currentThread().getName());
threadLocal.set(person);
threadLocal2.set("haha");
System.out.println(threadLocal.get());
System.out.println(threadLocal2.get());
System.out.println();
threadLocal.remove();
});
executorService.shutdown();
}
我的实际应用
举一个我的项目中使用场景:
背景:上一个项目在优化整体代码,把项目我负责的主体的实现方式做了调整,ThreadLocal在其中扮演了非常重要的角色。需求(使用场景):客户端有一个请求过来,Java程序根据请求报文的参数决定调用某个服务提供数据。相信大家都会有类似的需求,拿我们业务来说,请求报文如下:
{ "appid": "88888888", "userid": 80, "datatype": "***", "data":"{"dataids":[146,147,148]}" }
一个用户请求过来,需要根据报文中的datatype字段决定调取不同的数据提供服务。大体如下图所示:
在这里插入图片描述我在基类定义了一个ThreadLocal 对象来隔离PullDataNotification(封装请求信息)这个类成员变量,因为之前项目由于线程安全问题,每一个请求都是new 一个全新的Provider 处理来规避这个问题,看Cat上GC新生代很频繁,为了性能提升使用ThreadLocal 只创建一个对象,降低对象的频繁创建和销毁。
public abstract class CommonCreditProvider<E extends CreditArgument> implements IDataProvider{
//基类定义ThreadLocal对象,如果有多个变量做线程隔离可以初始化多个对象
protected ThreadLocal<PullDataNotification> notificationThreadLocal = new ThreadLocal<>()
//主要处理函数 实际处理逻辑在process,由子类完成
@Override
PullDataResult getData(PullDataNotification notification) throws Exception {
PullDataResult pullDataResult = null
try{
notificationThreadLocal.set(notification)
getThreeElements()
E creditArgument = initArguments()
pullDataResult = process(creditArgument)
}catch (Exception ex){
throw ex
}finally{
//*****使用完手动remove******
notificationThreadLocal.remove()
}
return pullDataResult
}
}
重点:关注一个细节,我finally 代码块手动remove把线程变量从Thread对象的ThreadLocalMap中移除,因为如果不这么做可能有内存泄漏风险。
原因:看源码,引用链,Thread -> ThreadLoalMap -> Entry[ WeakRefrence, Object] 因为线程对象(Tomcat请求线程池等)存在,线程维护的Map的Entry数组如果不手动清除也还存在,根据JVM GC的根路径定位,那数组中Entry的key,value是变量都在链上,所以会有内容泄漏的风险,所以要在使用完之后手动调用remove()函数清除。
后记
网上的很多文章,讲ThreadLocal中的ThreadLocalMap存放的key是线程对象,value是设置的线程局部变量,乍一看觉得挺有道理的,也算实现了线程数据隔离。但是仔细想不合理,如果有多个变量都要线程隔离,key又是线程对象,那key不够用啊!看源码,发现网上博客很多写的是错的,所以还是以后遇到有疑问的多想想What? How?Why?这东西是什么?怎么用? 为什么这么实现?合理吗?
这里没有敷衍的复制粘贴,博眼球的面试资料分享,有的只是尽可能清晰的讲清个人开发中遇到的一个个问题和总结。欢迎大家关注Java流水账,纯粹的个人技术公众号。
在这里插入图片描述