ThreadLocal解析
一、简介
首先我们需要知道Thread.currentThread()获取当前线程对象,同一个线程每次获取的都是同一个Thread对象。
ThreadLocal主要是用来将数据与线程绑定。
ThreadLocal其实主要思想就是将数据保存在当前线程对象,然后同ThreadLocal对象去操作保存的对象。主要方法包括设置值,取值,移除等操作。
二、类的结构
还有两个内部类的结构
三、主要方法
initialValue()
protected T initialValue() {
return null;
}
可以看到这是一个钩子方法,所以我们可以重写该方法,获得更好的实现效果。
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,则直接设置值调用ThreadLocalMap的set方法,也就是
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();
}
int i = key.threadLocalHashCode & (len-1);
这里用到了神奇的数字0x61c88647,用它可以将值更优的放到2的幂大小的数组中,举个栗子可以将打印内容作为数组位置。
public static void main(String[] args) {
int t = 0x61c88647;
AtomicInteger atomicInteger = new AtomicInteger();
int size = 1<<4;
for (int i = 0; i < size; i++) {
atomicInteger.getAndAdd(t);
System.out.println(atomicInteger.get()&(size-1));
}
}
threadLocalHashCode这里是final修饰的所以直接使用该值不会修改。所以一般同一个threadLocal对象进来获取的是同一个位置。
ThreadLocalMap是保存在Thread中。
关系如下图所示
set是将key为ThreadLocal,value 为我们自定的对象的map。
当map不为空的时候将value与当前线程绑定,创建ThreadLocal.ThreadLocalMap,然后将value与当前线程绑定,其实只有一句代码
t.threadLocals = new ThreadLocalMap(this, firstValue);
get()
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();
}
getMap(t)是获取当前线程绑定的ThreadLocal.ThreadLocalMap对象,这个对象保存在Thread对象里的threadLocals属性,内容值也就是ThreadLocal的内部类ThreadLocalMap的实例,即我们设置过的key是当前ThreadLocal对象,value为我们自定义的对象。
如果为空则返回的是调用setInitialValue获取的值,这里就跟方法initialValue有关联了。我们可以看一下实现。
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;
}
根据此我们可以重写initialValue()方法,放入我们的默认值。
四、项目中使用
情景:老项目因为一些原因很多接口要添加参数,如果接口里面都添加参数,调用方法添加参数,但是不想影响结构。那么可以用ThreadLocal来设置和获取参数的方式,防止对已有内容造成影响,首先得弄清楚,你对数据的处理是不是在同一个线程中。
对于接口添加参数的做法就是在拦截器里获取参数,添加到ThreadLocal中,后面哪里使用哪里取出来就好了。如果后面代码多线程的话不要直接使用。而存取可以用工具类的方式。比如:
public final class TestUtil{
private static final String KEY= "参数";
private static final ThreadLocal<Map<String, String>> sessionStore = new ThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue() {
return new HashMap<>();
}
};
public static void add(String content) {
Map<String, String> sessionMap = sessionStore.get();
sessionMap.put(KEY, content);
}
public static Locale get(){
return sessionStore.get().get(KEY);
}
public static void remove() {
sessionStore.remove();
}
}
这里用了static修饰ThreadLocal,如果对象很大,线程时间很长的情况下建议不要用static修饰ThreadLocal,因为默认情况下因为用了弱引用的方式,也就是当ThreadLocal只有弱引用没有其他引用的时候,该对象将自动被GC,在调用调用ThreadLocal其他方法的时候可以清理value对象。根据此特性,在消耗比较大的情况下,可以做一定优化。避免内存泄露。
五、引申
其实里面用的一些方式也可以在其他项目中使用
- 弱引用的使用
- 神奇的数字0x61c88647的使用
- 钩子方法的使用
- ThreadLocal本身的使用
如果文中有错误的地方,欢迎指出,以免造成误导。