ThreadLocal笔记
Mybatis-PageHelper 是一个MyBatis分页插件,在MyBatis配置里面配置拦截器,之后在需要分页的地方调用插件的方法就可以自动注入limit字段,实现分页功能。
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);
其中的startPage和offsetPage都是静态方法,没有上下文的传递,那么怎么有效的把数据传递到拦截器呢?查看源码找到了ThreadLocal这个东西,类似的东西还有SecurityContextHolder.getContext() 也是通过ThreadLocal获取上下文的。
第一次看到SpringMVC在控制器里面出现直接用静态函数获取当前用户信息,我是非常惊讶的。
public String handler() {
User u = UserUtils.getUser();
}
不需要任何参数,如何获取当前请求会话的信息,查看了代码之后,明白上下文都存储在当前进程里面就觉得没什么奇怪了,在java里面,当前的进程都是一个隐形的上下文。
使用
首先创建一个ThreadLocal对象
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
之后在每个线程使用set
/get
互不影响,而且通常用static
修饰。
new Thread(new Runnable(){
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " set value:0");
threadLocal.set("0");
Thread.sleep(1000*2);
System.out.println(Thread.currentThread().getName() + " get value:" + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " set value:1");
threadLocal.set("1");
System.out.println(Thread.currentThread().getName() + " get value:" + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
输出
Thread-0 set value:0
Thread-1 set value:1
Thread-1 get value:1
Thread-0 get value:0
原理
调用ThreadLocal#get
函数,内容如下,首先查询当前线程,之后从线程里面拉取ThreadLocalMap对象,如果存在就设定值,如果不存在就初始化。
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就是一个Map对象,getMap方法就是获取就是线程的Thread.currentThread().threadLocals
属性。之后就是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)]) {
if (e.refersTo(key)) {
e.value = value;
return;
}
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
代码里面的索引i
是性能上的优化,逻辑上还是查询空的插槽然后换入新值,期间包括各种性能上的优化。同理可以反推ThreadLocal#set
工作原理。
for(i=0;i<tab.lenght;i++){
if(tab[i].key == key){
tab[i].value = newVal;
return;
}
if(tab[i].value==null){
tab[i].value = newVal;
return;
}
if(tab[i]==null){
tab[i]= {key:key,value:newVal};
return;
}
}
比较有意思的是,获取数据的这一段,只用threadLocalHashCode
和表长度进行模运算,运算结果作为索引直接碰撞,如果碰撞结果符合直接返回,如果错开了,那么才进入getEntryAfterMiss
方法循环查询要的内容。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
总结
每条线程上都有独立的Thread对象
Thread对象保存一个容器Thread.threadLocals属性,可以用来存储上下文
ThreadLocal实例本身就是这个上下文容器里面获取值的
key
,所以一般用都添加static修饰符
还有其他的知识点:
- Thread.threadLocals对应的类型是ThreadLocal.ThreadLocalMap
- 最终数据存在 ThreadLocal.ThreadLocalMap.table
- ThreadLocal.ThreadLocalMap.table 是List
- 源代码里面很大一块都是针对这个列表的快速查询、扩容等操作,这个列表大小永远为2的N次幂
- 列表的条目Entry是一个WeakReference加 value的结构,WeakReference指向ThreadLocal,就是上面第3点说的key
- ThreadLocal实例化的时候存在一个Hash增量0x61c88647,问我工作原理是什么,我也不懂
我并不怎么喜欢用ThreadLocal,使用这个实现会造成一种逻辑上的撕裂感,给我一种怀疑我在写什么东西的感觉。kotlin的协程也有上下文对象CoroutineScope,Go就没有这个,因此每次都需要显式的传递上下文对象,可能是接收了if err!= nil
的洗礼了,这种麻烦的传递上下文的方式我并不讨厌。