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);

其中的startPageoffsetPage都是静态方法,没有上下文的传递,那么怎么有效的把数据传递到拦截器呢?查看源码找到了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);
        }

总结

  1. 每条线程上都有独立的Thread对象

  2. Thread对象保存一个容器Thread.threadLocals属性,可以用来存储上下文

  3. ThreadLocal实例本身就是这个上下文容器里面获取值的key,所以一般用都添加static修饰符

还有其他的知识点:

  1. Thread.threadLocals对应的类型是ThreadLocal.ThreadLocalMap
  2. 最终数据存在 ThreadLocal.ThreadLocalMap.table
  3. ThreadLocal.ThreadLocalMap.tableList
  4. 源代码里面很大一块都是针对这个列表的快速查询、扩容等操作,这个列表大小永远为2的N次幂
  5. 列表的条目Entry是一个WeakReferencevalue的结构,WeakReference指向ThreadLocal,就是上面第3点说的key
  6. ThreadLocal实例化的时候存在一个Hash增量0x61c88647,问我工作原理是什么,我也不懂

我并不怎么喜欢用ThreadLocal,使用这个实现会造成一种逻辑上的撕裂感,给我一种怀疑我在写什么东西的感觉。kotlin的协程也有上下文对象CoroutineScope,Go就没有这个,因此每次都需要显式的传递上下文对象,可能是接收了if err!= nil的洗礼了,这种麻烦的传递上下文的方式我并不讨厌。