Ref:
https://mp.weixin.qq.com/s/8Ql-5kaUtxiCWyHR6uPPBw
https://www.cnblogs.com/fengzheng/p/8690253.html
1.使用场景
ThreadLocal 归纳下来就 2 类用途:
- 保存线程上下文信息,在任意需要的地方可以获取。
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失。
根据 ThreadLocal 的特性,一些场景:
- 同一线程在某地方进行设置,在随后的任意地方都可以获取到(复杂业务场景中代替一些参数的显式传递),从而可以用来保存线程上下文信息。
- 每个请求怎么把一串后续关联起来,就可以用 ThreadLocal 进行 set,在后续的任意需要记录日志的方法里面进行 get 获取到请求 id,从而把整个请求串起来。
- Spring 的事务管理,用 ThreadLocal 存储 Connection,从而各个 DAO 可以获取同一 Connection,可以进行事务回滚,提交等操作。
ThreadLocal 的这种用处,很多时候是用在一些优秀的框架里面的,一般我们很少接触,反而下面的场景我们接触的更多一些!
线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!
ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。但是 ThreadLocal 也有局限性,我们来看看阿里规范:
每个线程往 ThreadLocal 中读写数据是线程隔离,互相之间不会影响的,所以 ThreadLocal 无法解决共享对象的更新问题!
由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全,以及避免了某些情况需要考虑线程安全必须同步带来的性能损失!!!
使用示例
public class ThreadLocalTest {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
// 设值
threadLocal.set(i);
System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal1").start();
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal2").start();
}
}
Output:
...
threadLocal2====null
threadLocal1====98
threadLocal2====null
threadLocal1====99
2.ThreadLocal 实现
Thread、ThreadLocalMap、ThreadLocal 总览图:
Thread 类有属性变量 threadLocals (类型是 ThreadLocal.ThreadLocalMap),也就是说每个线程有一个自己的 ThreadLocalMap
,所以每个线程往这个 ThreadLocal 中读写是隔离的,互相不会影响。
一个 ThreadLocal 只能存储一个 Object 对象,如果需要存储多个 Object 对象那么就需要多个 ThreadLocal。
如图:
看到上面的几个图,大概思路应该都清晰了,我们 Entry 的 key 指向 ThreadLocal 用虚线表示弱引用 ,下面我们来看看 ThreadLocalMap:
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
static class ThreadLocalMap {
private Entry[] table;
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
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();
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 删除无用entry(key弱引用被回收 value尚存)
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
}
}
java 对象的引用包括 : 强引用,软引用,弱引用,虚引用 。
因为这里涉及到弱引用,简单说明下:
- 弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。
当仅仅只有 ThreadLocalMap 中的 Entry 的 key 指向 ThreadLocal 的时候,ThreadLocal 会进行回收的。
ThreadLocal 被垃圾回收后,在 ThreadLocalMap 里对应的 Entry 的键值会变成 null,但是 Entry 是强引用,那么 Entry 里面存储的 Object,并没有办法进行回收,所以 ThreadLocalMap 做了一些额外的回收工作。
虽然做了但是也会存在内存泄漏风险(最佳实践使用静态变量)。
3.实践
很多时候,我们都是用在线程池的场景,程序不停止,线程基本不会销毁
由于线程的生命周期很长,如果我们往 ThreadLocal 里面 set 了很大很大的 Object 对象,虽然 set、get 等方法在特定的条件会调用进行额外的清理,但是 ThreadLocal 被垃圾回收后,在 ThreadLocalMap 里对应的 Entry 的键值会变成 null,但后续无操作 set、get 等方法。
- 应该在我们不使用的时候,主动调用 remove 方法进行清理。使用完 ThreadLocal ,手动调用 remove () 方法,例如 Tomcat 的 Session 例子,如果不在拦截器或过滤器中处理,不仅可能出现内存泄漏问题,而且会影响业务逻辑(在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现);
- 把 ThreadLocal 定义为 static, 由于 ThreadLocal 有强引用在,那么在 ThreadLocalMap 里对应的 Entry 的键会永远存在,那么执行 remove 的时候就可以正确进行定位到并且删除。
最佳实践: ```java public class ContextHolder {
private static final ThreadLocal