ThreadLocal学习笔记
最近看一些面经,好像很多时候都有提到ThrealLocal的实现原理,之前在学习多线程的时候似乎没有太注意ThreadLocal这个本地线程,现在看看源码,学习一下。
ThreadLocal的实现原理
ThreadLocal是线程内部的数据存储类,通过它可以指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取数据.
自定义ThreadLocal实现
自定义实现一个ThreadLocal,先定义一个map,将当前线程作为key,线程拥有的数据作为值存入map,实现value为当前线程私有。
/**
* @ClassName MyThreadLocal
* @Description: 自定义一个简易ThreadLocal
* @Author zhouyang
* @Date 2019/5/22 下午9:55.
*/
public class MyThreadLocal<T> {
//基于map实现
private Map<Thread,T> values = new HashMap<>();
//存值
public synchronized void set(T value){
values.put(Thread.currentThread(),value);
}
//取值
public synchronized T get(){
return values.get(Thread.currentThread());
}
}
自定义可以完成基本功能,但是在多线程中性能较差,会发生冲突。
源码解读
源码中的set和get方法,保证了各个线程之间的数据互不干扰。
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 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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取。
所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
ThreadLoalMap
ThredLocalMap是ThreadLocal中的一个内部类,用于存储线程的ThreadLocalMap对象。
static class ThreadLocalMap {
// 自定义Entry类用于存储<ThreadLocal, Value>键值对.
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private static final int INITIAL_CAPACITY = 16;
private int threshold;
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
// 使用数组来模拟实现Map.
table = new Entry[INITIAL_CAPACITY];
// 使用ThreadLocal的HashCode来生成下标,尽量减少哈希碰撞
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// 设置扩容resize时的阈值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。
hash冲突
在ThreadLocalMap中,数据的存入源码:
/**
* 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();
}
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
- 1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
- 2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
- 3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
内存泄露
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("crazyang");
// 其它业务逻辑
} finally {
localName.remove();
}