Java ThreadLocal
之前在写SSM
项目的时候使用过一个叫PageHelper
的插件
可以自动完成分页而不用手动写SQL limit
用起来大概是这样的
最开始的时候觉得很困惑,因为直接使用静态成员函数,那么就意味着如果有别的线程同时执行,可能会导致一些并发错误
答案是不会,因为PageHelper
内部实现是使用到了ThreadLocal
这个对象的,每个线程单独使用一个Page对象
百度了一下,发现ThreadLocal是一个提供类似线程内部的局部变量
我们来看一下ThreadLocal的源代码:初始化的时候涉及到这几个变量
private static AtomicInteger nextHashCode =
new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
每次创建一个ThreadLocal的时候就会给这个?ThreadLocal分配一个hashcode
为什么不是每次increateAndGet 注释里面有解释:
连续生成的散列码的区别
隐式顺序线程局部ID进入近最优分布
两个大小表的幂的乘法哈希值。
首先看一下get方法
//ThreadLocal.java
public T get() {
//首先获取当前的Thread
Thread t = Thread.currentThread();
//通过当前Thread尝试获取 ThreadLocalMap
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;
}
}
//当前Thread并没有初始化map或者Thread值,进行初始化操作
return setInitialValue();
}
通过名字可以知道ThreadLocalMap似乎是个Map
我们先查看一下getMap
发现这个map是存储在Thread 里面的,包作用域,用户不可见
//Tread.java class Thread
ThreadLocal.ThreadLocalMap threadLocals = null;
我们现在看一下这个ThreadLocalMap的定义
static class ThreadLocalMap {
//定义键值对,是一个WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
//ThreadLocal里面保存变量的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//默认初始容量
private static final int INITIAL_CAPACITY = 16;
//桶
private Entry[] table;
private int size = 0;
初始化:
我们可以看到第一次初始化的时候是使用firstKey的threadLocalHashCode(firstKey指的是外部的this)刚才提到的初始化的时候分配的一个hashcode,具体桶的位置跟hashmap类似都是(桶-1)&hashcode
//ThreadLocal.java
//static class ThreadLocalMap
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);
}
ThreadLocalMap里面的 get方法
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);
}
ThreadLocalMap解决hash冲突的方法并不是用链表,而是使用线性探测法
这也就解释了为什么分配的hashcode不应该是连续的原因,否则一旦出现hash冲突,线性探测找到一个可用的空间或者key对应的值非常艰难
我们来看一下是如何实现线性探测的:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//当这个位置不是空的时候继续探测
while (e != null) {
ThreadLocal<?> k = e.get();
//判断key是不是相等,如果相等说明找到了
if (k == key)
return e;
//这里不太理解,查了一下别人的分析
//k==null说明这个key已经被释放掉,需要清理掉
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);//((i + 1 < len) ? i + 1 : 0);就是下一个空间,如果到末尾就从头开始
e = tab[i];//下一个空间里面的值
}
return null;
}
//这个函数不太理解
//我猜大概意思就是需要别的地方有一个ThreadLocal引用否则ThreadLocal可能被清理掉
//弱引用会被GC标记存活
//这个做的应该是标记为null之后,把后面的值放到前面,否则再次get的时候碰到null就找不到了
/**
* 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
* 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
* 另外,在过程中还会对非空的entry作rehash。
* 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//直接置null
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)) {
//每次线性探测一个格子直到找到null
ThreadLocal<?> k = e.get();
//key为null说明需要清理掉
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;
}
并发安全问题:
代码中没有使用到任何锁和同步,为什么还是安全的
因为每个线程操作的ThreadLocalMap都是每个线程自带的,当然不用同步啦
具体使用:比如说解决SimpleDateFormat的问题
这样每个线程只会创建一个
具体的线性探测hash可以看着里面的图