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的洗礼了,这种麻烦的传递上下文的方式我并不讨厌。