ThreadLocal,线程变量,是一个以ThreadLocal对象为键,任意对象为值 的存储 结构。该结构附着于线程之上。每个线程都保存一份原始变量的副本,每个线程对ThreadLocal变量的修改互不影响。可以通过接口get()返回当前线程中保存的ThreadLocal变量的值,通过接口set()设置当前线程中保存的ThreadLocal变量的值,只要当前线程一直存活,该ThreadLocal变量在当前线程中的副本就会一直存在。所以ThreadLocal和线程安全没有关系,并且适合在后台记录一个线程连续的行为。
ThreadLocal的几个主要接口
接口 | 说明 |
---|---|
protected T initialValue() | 返回当前线程中该ThreadLocal变量的初始值,当一个线程首次获取该变量时,会触发该方法。 |
public T get() | 返回ThreadLocal变量在当前线程副本的值,如果当前线程的变量没有值,会调用initialValue进行初始化并返回该值 |
public void set(T value) | 设置ThreadLocal变量在当前线程的副本的值 |
public void remove() | 移除当前线程中该ThreadLocal变量的值 |
JavaDoc给的一个例子:
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
一.ThreadLocal基础接口
我们可以通过源码来分析ThreadLocal的实现原理。
以ThreadLocal的get方法为例:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
private void set(ThreadLocal key, Object value) {
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
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
get方法的步骤如下:
- 获取当前调用的线程
- 获取线程拥有的ThreadLocalMap变量
- ThreadLocalMap内部有一个Entry数组,以ThreadLocal变量的hash值计算Entry数组的下标,得到对应Entry;否则需要初始化该线程的ThreadLocalMap。
set方法的步骤余get类似,只是做的是相反的操作。
二.ThreadLocal内部数据结构ThreadLocalMap
ThreadLocalMap是ThreadLocal的一个内部类,该结构也是一个专门用于维护线程局部变量的hash map,每个线程都有一个ThreadLocalMap。
ThreadLocal.ThreadLocalMap threadLocals = null;
每个ThreadLocalMap有一个Entry数组,默认大小为16。Entry数组的下标值是通过ThreadLocal变量的hashCode对数组长度取余的结果。如下示:
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
同时,该map是通过碰撞法解决hash冲突的问题,map内部元素数量超过阈值会进行rehash操作。
有一点需要注意,ThreadLocalMap的Entry数组里的元素是WeakReference。
private Entry[] table;
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的作用来知晓。WeakReference作为Java中的四种Reference类型之一,WeakReference引用的实例的存在是不会影响其引用的对象的回收行为。被WeakReference引用的对象只能生存到下一次垃圾收集发生前。当垃圾收集发生时,如果该WeakReference引用的对象除了被该WeakReference引用外,没有别的在GC Roots引用链的引用实例引用该对象的情况下,无论内存是否足够,该对象一定会被回收掉。
Java的引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这四种引用强度依次减弱。
1.强引用:代码中普遍存在的,类似Object obj = new Object()的这类引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象。
2.软引用:用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常,JDK提供了SoftReference来实现软引用。
3.弱引用:用来描述非必须对象,但它的强度比软引用更弱一些,被弱引用引用的对象只能生存到下一次垃圾收集发生之前。当垃圾收集发生时,无论内存是否足够,都会回收掉只被弱引用关联的对象。JDK提供了WeakReference
4.虚引用:它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个通知。JDK提供了一个PhantomReference类来实现虚引用。
根据这些结构,我们可以描绘出这些对象之间的一些引用关系:
根据前面提到的WeakReference的特性,我们可以考虑一种情况,如果一个ThreadLocal对象没有外部强引用的引用,则发生GC时,该ThreadLocal对象会被回收清除,此时ThreadLocalMap中就会出现key为null的Entry,我们就无法访问这些key为null的val,如果该线程一直不退出(线程池中的线程),于是就会一直存在一条强引用链:Thread Ref->Thread Object->ThreadLocalMap->Entry->val,从而导致该val的对象无法回收,发生内存泄漏。
而实际上,ThreadLocalMap使用ThreadLocal变量的WeakReference作为Entry的key是考虑到这种情况了的。在ThreadLocalMap的getEntry与set方法中对key引用对象为nul的Entry都进行了擦除处理。源码如下:
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);
}
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
//
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除staleSlot处的Entry
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;
}
在调用getEntry的过程中,如果key为null,就会进行Entry擦除操作,保证该Entry内的val不会存在强引用链,于是下次GC该对象就可以被回收。同样的set函数中也存在类似的擦除逻辑。但是这依赖于对ThreadLocalMap中Entry数组元素的获取与设置必须通过这两个函数的保证。所以很多时候我们需要主动调用ThreadLocal的remove函数,这会调用ThreadLocalMap的remove方法,主动擦除一个ThreadLocal变量对应的Entry元素。
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
三.ThreadLocal的使用场景
- 在一次接口调用中传递log参数
private static ThreadLocal<Map<String, Object>> logParamMap = new ThreadLocal<Map<String, Object>>();
public static Object getLogParamMap(String key) {
if (logParamMap.get() == null)
return null;
return logParamMap.get().get(key);
}
public static void setLogParamMap(String key, Object obj) {
if (logParamMap.get() == null) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(key, obj);
logParamMap.set(map);
} else {
logParamMap.get().put(key, obj);
}
}
public static void removeLogParamMap() {
logParamMap.remove();
}
再一次接口调用中,中间用到的各个方法都可以调用setLogParamMap方法添加日志参数,而不必到处传递String参数。
调用处:
StatisticsLogs.setLogParamMap("var1", var);
StatisticsLogs.setLogParamMap("var2", var2);
StatisticsLogs.setLogParamMap("var3", var3);
StatisticsLogs.setLogParamMap("var4", var4);
- 减少实例创建
public class SafeDateFormatter {
private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
@Override
public SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static SimpleDateFormat getDateFormat(){
return dateFormat.get();
}
}
SimpleDateFormat不是线程安全的,所以很多时候我们使用的时候没办法共享该变量,而是在每次使用时都会创建。通过这种方法获取SimpleDateFormat变量,每个线程拥有一个SimpleDateFormat实例。。这在站点后台大量使用线程池的情况下十分划算,只要线程不退出,就会一直拥有该实例。而不需要每次使用时重新new一个。
总结
通过源码分析,我们可以清楚知道,ThreadLocal并不是像很多人误解的那样,能够解决共享变量的并发访问问题。实际上,ThreadLocal变量使每个线程都保存一份该变量的副本,各线程对副本的操作互不影响(这也不符合我们对多线程访问下的共享变量的期待-即正确的同步机制能够保证共享变量的修改对各个线程的可见性)。因此,ThreadLocal最适合每个线程都需要持有某个实例,且该实例使用很频繁的场景。