1.使用场景
保存每个线程特有的本地缓存数据,天生的线程安全数据结构,但是在实际使用过程中可能会遇到一些坑。在实际项目中,例如:session数据,事务资源,servlet中的request和response数据等一般都是放在ThreadLocal中,可以做到线程隔离,每个线程只能访问到自己线程相关的数据,不看源码,我们理解里面肯定是个map结构,key应该是与线程相关的唯一标识,value就是需要保存的数据。
2.根据源码来研究其实现原理
以最简单的设置一个int值为例
ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(10);
构造函数是空实现,没有任何操作
再来看看set设置进行什么操作?
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap是ThreadLocal静态内部类,通过分析ThreadLocalMap源码,总结出其与hashmap的一些区别:
1.HashMap是数组(槽)+链表,根据hash值与数组长度的模除得到数组的位置,如果产生冲突,就会放到链表中,而ThreadLocalMap只有数组没有链表,这样设计的原因是为了简单,因为当链表数量超过一定的数量,需要转化为红黑树加快速度,红黑树实现比较复杂,所以作者为了考虑到这些没有用到冲突链表结构;而且系统中的ThreadLocal变量肯定不会像HashMap那样,会有非常多的数据,不会产生非常多的冲突,基于以上这些ThreadLocalMap相较于HashMap做了一些简化处理;
2.引入了弱应用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry继承了WeakReference,WeakReference与强引用的区别是:强应用只有在GCRoots找不到的时候才会去释放;垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
为了了解弱应用,通过例子来说明:
WeakReference<Integer> weakReference = new WeakReference<Integer>(1000);
System.out.println("gc前:"+weakReference.get());
System.gc();
System.out.println("gc后:"+weakReference.get());
运行结果:
gc前:1000
gc后:null
GC后内存是足够的,但是弱引用空间还是释放了,因为1000这个值只有弱引用指向它,满足弱应用的回收条件,不管此时内存是否足够都会进行释放。再看另一个例子:
Integer num = 1000;
WeakReference<Integer> weakReference = new WeakReference<Integer>(num);
System.out.println("gc前:"+weakReference.get());
System.gc();
System.out.println("gc后:"+weakReference.get());
执行结果:
gc前:1000
gc后:1000
这个例子没有得到我们想要的结果,没有进行释放,然后稍微修改一下程序:
Integer num = 1000;
WeakReference<Integer> weakReference = new WeakReference<Integer>(num);
System.out.println("gc前:"+weakReference.get());
num = null;
System.gc();
System.out.println("gc后:"+weakReference.get());
在gc之前加了一行num = null;,然后查看执行结果:
gc前:1000
gc后:null
这个得到了我们想要的结果。为什么????
Integer num = 1000;
WeakReference<Integer> weakReference = new WeakReference<Integer>(num);
这两行代码的结果是,1000这个值会有两个引用指向它,包括一个强引用(num)和一个弱引用(weakReference),所以gc后它不会释放;但是在gc之前加入了num = null;这行代码,会将指向它的强引用干掉了,只留下了弱引用指向1000,然后进行GC后,此时满足了弱引用的释放条件,数据正常释放了。所以弱应用的释放条件是弱引用指向的数据没有强应用(或者软引用--GC后内存不够才会释放)指向它。
回到ThreadLocal来,为什么ThreadLocal需要使用弱引用????
继续看ThreadLocal中的set方法
先获取当前线程,getMap获取线程Thread对象的threadLocals字段,去查看一下此字段的申明ThreadLocal.ThreadLocalMap threadLocals = null;,当threadLocals为null时,createMap创建新的ThreadLocalMap对象,并且赋初始化值,如果不为null,说明之前当前线程已经初始化创建过了ThreadLocal对象,直接将ThreadLocal对象作为key,加入到threadLocals中。总结一下:当新建一个ThreadLocal对象时,先获取当前所在线程,然后获取线程对象中的threadLocals字段,该字段是一个map结构,然后将ThreadLocal对象作为key,需要存储的值作为value,加入到map中,所以如果当前线程需要创建多个ThreadLocal对象,则threadLocals中就会有多个元素。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);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
代码的主要逻辑:遍历ThreadLocalMap中数组的所有槽,当槽元素的key和新增的ThreadLocal对象相同,将value值进行覆盖;如果key值为null,则调用replaceStaleEntry方法;如果找到的槽元素为null,说明该槽位置从未使用过或者使用过但是已经释放,则在该位置填充数据;当数组槽元素数量超过threshold(默认是数组元素长度的2/3),会进行扩容和重新hash。cleanSomeSlots是做无效数据的清理,Entry存在key为null的元素,将这部分数据value清理掉。
ThreadLocal采用弱引用的原因,以上面的例子来说明:
ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(10);
ThreadLocal对应的内存空间目前有两个引用指向它,第一个是上面新建的local,是强引用,还有就是当前线程ThreadLocal.ThreadLocalMap threadLocals有一个元素Entry[] table,如果不采用弱引用,则当local引用设置为null时,虽然内存空间local不再指向它,但是还有table指向它,那么这个内存空间的数据是不会释放的,就会造成内存的泄漏,所以当设置为弱引用后,一旦local强引用不指向了,就只有table一个弱引用指向它,所以满足了内存释放的条件,GC后就会进行内存的释放,避免了产生内存泄漏。
3. ThreadLocal实际项目中可能会遇到的问题
1.内存泄漏
Entry的key和value都可能会产生泄漏
key的泄漏通过弱引用已经解决了,但是value是强引用指向,如果不做任何处理,value值是不会释放的(说法不安全正确,因为在ThreadLocal的处理过程中,如果遇到key为null的槽,它会强制将value值进行清理,但是这个过程是不确定的,不能保证完全的实时性),所以ThreadLocal提供了remove函数,下面来分析来remove函数,先上源码:
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)]) {
//找到对应的槽
if (e.get() == key) {
//清除key的弱引用
e.clear();
//清除无效的槽位
expungeStaleEntry(i);
return;
}
}
}
2.线程池带来的问题
实际项目中,很少会单独创建线程在线程中使用ThreadLocal,基本上都是线程池,在线程池中,核心线程是常驻内存的,如果在使用完ThreadLocal后不去手动调用remove清除,如果使用的是核心线程,则当该线程执行完,因为核心线程是不会释放的,所以下次这个线程还是能获取到数据的,这样会导致很严重的数据安全问题,在实际项目中就遇到这样的问题,我们将租户的一些登录数据放在ThreadLocal中,然后莫名的另一个租户获取到的登录数据竟然一样的,经过排查发现,我们没有正确去进行释放,我们原错误伪代码:
private ThreadLocal<User> user = new ThreadLocal<User>();
//设置用户的登录数据
user.setLoginData(new User());
//执行业务逻辑1
executeBiz1();
//执行业务逻辑2
try{
executeBiz2();
} finally{
user.remove();
}
这段代码存在的问题是:当执行executeBiz1异常时,代码直接抛出异常,导致remove没有调用,所以导致了当前租户存在内存的数据可能被其他租户访问到,因为tomcat肯定也是用的线程池。
3.异步任务丢失数据
如果在A线程里面再运行一个新线程,那么在A线程创建的ThreadLocal数据,在新线程是获取不到的,这个时候一般的做法是,在新线程运行代码的起始处将A线程创建的数据set进去,这样新线程就能获取到A线程的数据了。
4.异步事务失效
数据库事务里面的连接池等资源也是存放在ThreadLocal里面的,所以事务也是线程相关的,如果在事务里面开启另一个线程做数据库的增删改操作,对事务是无效的,如果确实需要,那么在新线程里再新建事务提交。
参考文章:
https://www.cnblogs.com/micrari/p/6790229.html
网友评论