ThreadLocal是什么?
ThreadLocal是线程本地变量,若创建了一个ThreadLocal变量,则访问这个变量的每个线程都有这个变量的本地拷贝,当多线程操作这个变量时,实际操作的是自己本地内存的变量,从而起到线程隔离作用,避免线程安全问题。
ThreadLocal 使用场景
作为副本,当某数据以线程作为作用域并且不同线程有不同副本时,可以考虑使用 保存线程的上下文信息,在任意需要地方都可获取,避免显式传参 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失
ThreadLocal内部实现
每个线程都有一个 ThreadLocalMap(ThreadLocal内部类),Map 中元素的键为 ThreadLocal,而值对应线程的变量副本。Map 是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
// ThreadLocal 的用法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
调用 threadLocal.set() --> 调用 getMap(Thread) --> 返回当前线程的 ThreadLocalMap < ThreadLocal, value >-->map.set(this, value),this 是 ThreadLocal
调用 get() --> 调用getMap(Thread) --> 返回当前线程的 ThreadLocalMap<ThreadLocal, value>-->map.getEntry(this),返回value。
这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。
对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。
由于Tomcat线程池的原因,我最初使用的”线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务treadLocal的remove()方法,将”线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。
ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。
//ThreadLocal
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//ThreadLocal.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;
}
// 代表key弱引用已被回收
if (k == null) {
// 如果被回收 需要替换
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
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).
// 向前遍历 检查是否有已经被回收的key
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.
// 遇到相同的需要替换value
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);
}
ThreadLocal造成的内存泄露
原因
在jvm中栈内存为线程私有,其中存储对象引用。堆内存为线程共享,存储了对象实例。所以,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。
ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。
解决内存泄漏
很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal<String> localVariable = new ThreadLocal();
try {
localVariable.set("---”);
...
} finally {
localVariable.remove();
}
为什么key要设计成弱引用
key设计成弱引用同样是为了防止内存泄漏。 若key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLocal的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。
ThreadLocalMap 和 HashMap 区别
HashMap 的数据结构是数组+链表,HashMap 是通过链地址法解决hash 冲突的问题,HashMap 里面的Entry 内部类的引用都是强引用。 ThreadLocalMap的数据结构仅仅是数组,ThreadLocalMap 是通过开放地址法来解决hash 冲突的问题,ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用