Java并发系列-ThreadLocal
前言
面试过程中,并发知识相关中ThreadLoacl也是面试官爱问的一个点,小伙伴们一起看下下面这几个问题
- ThreadLocal的原理是什么,他是如何解决并发访问相关问题的
- ThreadLocal为什么会造成内存泄漏?如何解决内存泄漏问题,key为啥一定要使用弱引用
- ThreadLocal的应用场景
原理
线程隔离
ThreadLocal里面有一个ThreadMap类型的变量threadLocals,ThreadMap我们简单理解就是个Map,key是线程对象本身,value是要存储的对象。
public T get() {
//获取当前线程对象t
Thread t = Thread.currentThread();
//通过getMap方式获取到threadLoaclMap threadLocalMap保存所有的ThreadLocal变量
ThreadLocalMap map = getMap(t);
if (map != null) {
//map中存在值,返回Map中的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//Map没有初始化,新建一个ThreadLocalMap对象
return setInitialValue();
}
//新建ThreadLocalMap对象
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//不是空的添加到Map中
map.set(this, value);
else
//空的话创建map
createMap(t, value);
return value;
}
ThreadLocalMap
查看相关源码可以得知,threadLocalMap和我们常使用的Map不大一样,这玩意没有实现map接口,而是通过Entry数组存储key,value的,我们一起来看下(查看源码的时候主要关注get方式和set方法)
static class ThreadLocalMap {
//每一个Entry的key是一个弱引用。这样做的原因当变量key没有被其他多线使用的时候,自动回收ThreadLocal对象
static class Entry extends WeakReference<ThreadLocal<?>> {
//key是弱引用,value是强引用,这玩意你下次不退出,value一直存在(为甚这么设计?),
//key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景,我个人感觉方便垃圾回收
//这个时候再get,set,remove都需要主动清理value,我们可以看下get方法
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//获取Map中的值,get方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//找到key,直接返回对于的value
return e;
else
//找不到,往后面继续查询顺道清理key为null的数据
//为啥找不到还往后找,因为放元素的时候发现冲突的时候会继续往后面放
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
//从i位置开始遍历,寻找key能对应上的entry
ThreadLocal<?> k = e.get();
if (k == key)
//找到就返回
return e;
if (k == null)
//key为null,说明弱引用key被回收,那就把value回收掉
//expungeStaleEntry 函数会从当前位置开始,往后再找一段,碰到脏entry进行清理,碰到null结束
expungeStaleEntry(i);
else
//key不是要找的哪一个,出现hash冲突,处理冲突找下一个entry,继续往下寻找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
//往Map里面添加有一个值 set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//hash找到数据中的一个位置
int i = key.threadLocalHashCode & (len-1);
//位置没有被占用,说明没有冲突,直接插入即可,不用for循环,如果位置被占用,就一直往下找到可用的位置
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//数组中的值等于threadLocal的key,将值进行替换即可
e.value = value;
return;
}
if (k == null) {
//key为空,说明key已经被回收,新key、value覆盖,同时清理掉旧的value值
replaceStaleEntry(key, value, i);
return;
}
}
//将Entry放入tab中合适位置
tab[i] = new Entry(key, value);
int sz = ++size;
//大于阈值,需要进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
应用场景
session管理
请求到来的时候,将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。但要记得当请求处理完成后通过remove方法将当前Session信息清除即可。
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws Exception {
Session s = (Session) threadSession.get();
if (s == null) {
s = getSessionFactory().openSession();
//session请求。没有就放到ThreadLocal里面,线程之前隔离
threadSession.set(s);
}
return s;
}
DateFormat线程安全
DateFormat是线程非安全的(多个线程之间共享变量calendar,并修改calendar),多线程情况下,必须为每一次日期转化创建一个DateFormate,这里
/**
* 同学感兴趣的话可以看下多线程下SimpleDateFormat会出现什么问题
*/
public class DateUtils {
public static final ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
//每个变量线程副本
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
//调用
DateUtils.df.get().format(new Date());
问题解答
通过读源码我们一起总结下上面提到的几个问题回答的点
- ThreadLocal的原理是什么,他是如何解决并发访问相关问题的
- map线程隔离,保留副本
- ThreadLocal为什么会造成内存泄漏?如何解决内存泄漏问题,key为啥一点要使用弱引用
- 因为entry的key为弱引用,解决就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据
- 为啥使用弱引用
- 如果key 使用强引用:引用的ThreadLocal的对象被回收了,但entry没有被回收,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- 如果key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除
- 总结就是ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
- ThreadLocal的应用场景
- session管理
- DateFormat线程安全
闲谈
感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly呀,干货多多,湿货也不少(∩_∩)。