1、前言:
看名知意,本地线程,采用”空间换时间“的思想,每个线程持有该份数据的拷贝,线程互不影响,可以用来解决在一些环境下的线程安全问题。
2、关键变量和常量:
// 本地线程的 hashcode
private final int threadLocalHashCode = nextHashCode();
// 下一个线程的 hashcode 分配,原子性操作,初始值为 0
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 为了保证hashcode分布均匀,在原来的基础上加上这部分数值,这翻译成十进制的值为 1640531527
*/
private static final int HASH_INCREMENT = 0x61c88647;
// 返回 hashcode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
2.1、为啥要使用这个 HASH_INCREMENT :
可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。
3、ThreadLocal 常用代码分析:
和外部相关的几个常用的方法,如 get()、set()、remove()方法等,主要对这几个方法和在方法中被调用的本类方法来进行代码分析。
3.1、get() 方法
/**
* 返回当前线程的的值,如果没得返回初始化值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 值都存储在 ThreadLocalMap.Entry 里边,key 是一个当前 ThreadLocal 对象,后边都是正常的值获取,然后强转为 T
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果当前线程没有初始化值,则进行初始化值
return setInitialValue();
}
/**
* Thread 存有 ThreadLocalMap 变量
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 设置初始化值
*/
private T setInitialValue() {
// 是否有子类重写该方法,有则返回子类方法的 value ,没有返回 null
T value = initialValue();
Thread t = Thread.currentThread();
// 获取 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 如果已经存在 ThreadLocalMap 对象,就设置值,否则创建一个 ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* 根据子类重写的值返回一个初始化值,默认返回 null
*/
protected T initialValue() {
return null;
}
/**
* 创建一个 ThreadLocalMap 对象
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.2、set() 方法
/**
* 设置值,方法代码比较简单
*/
public void set(T value) {
// 常用的套路,获取当前线程,从当前线程的 threadLocals 变量 来获取 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
3.3、remove() 方法
/**
* 删除值,还是老套路,通过当前线程获取 ThreadLocalMap,然后当前 ThreadLocal 对象作为 key 来删除值
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4、ThreadLocalMap 静态内部类
上边的几个方法实现都简单明了,里面都涉及到了一个ThreadLocalMap对象,对外隐藏了内部的细节,很好的一个 API 实现思想,好了说远了,现在看看这个内部类的实现。
4.1、Entry,存储数据的数据结构
/**
* Entry 数组存放数据
*/
private Entry[] table;
/**
* 存放值的 Entry 对象,是一个 Map 结构,key 为 ThreadLocal 对象,需要注意的是这个继承于弱引用 WeakReference, 里边把 key 链接到了弱引用对象那了
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用:
当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他 strong reference(强引用)指向的时候, 如果这时 GC 运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
具体可以看看这篇(关于Java中的WeakReference - 简书)
4.2、getEntry() 方法
/**
* 获取 Entry 对象,java 目前关于定位 table 的 index 都是采取一个位运算的方式了。
*/
private Entry getEntry(ThreadLocal<?> key) {
// 位运算定位 index
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);
}
/**
* 没找到,分两种情况,
* 一种是里边根本没有,如 entry 为 null, 这种直接返回 null
* 另外一种还抱有希望的,但前提是你这个 threadlocal 不为 null,
* 前边说过 key 为 ThreadLocal 对象,被链接到了弱引用对象那了,所以一旦发生GC,这势必会被回收,所以这里需要判断下是否还存在
*/
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)
// 不存在,通过重新散列位于staleSlot和下一个空槽之间的任何可能发生冲突的条目,删除过时的条目
expungeStaleEntry(i);
else
// 存在,就找下一个 index
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
4.3、set() 方法
/**
* 设值,找到 key 所在位置,替换原有值,如果没找到,新建一个 slot 存放。
*/
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;
}
// 替换过时的 entry,为啥过时呢,也是上边说到的 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();
}
4.4、关于内存泄漏的问题
从 set() 、get()、remove() 方法里边,我们可以看到设计师考虑到这种情况了,会清理掉 key 为 null 的值。但是使用场景是多变的,可能出现使用完之后没有手动去 remove(),或者之后再也没有调用前边说到的那三个方法。这样才可能存在 key 为 null,但 value 却一直没有被清理的情况。所以为了保险起见,尤其是通过线程池这种来托管线程的方式,应该调用之后手动去 remove() ,避免内存泄漏的问题。
5、使用场景
5.1、每个线程需要有自身的一个实例
就拿最近开发的事来说,事件通知机制,公司原有的事件通知机制,是一个静态事件,添加、删除没啥问题,但通知就有问题了,可能会干扰到其他事件的发生。反应到UI上就是一个进度条混乱。对于这种情况,就可以利用这个ThreadLocal 来解决该问题。
同时还有常见的数据库连接的实现或者 web端 的 session 的保存。