从获取用户信息来看ThreadLocal做了些什么
系统登陆后后台service是怎么获取用户信息的呢,说实话,我也不知道,最近在做项目时遇到这么一个问题,就认真思考了下,在SpringMVC中找到了答案,就写下来记录下吧
在项目中有个WebUtil工具,这里面是有获取用户信息的方法的,但我是要找到他是怎么保存并获取这个用户信息的,当然不能只看的这么浅显
这个工具类中有个方法是获取request请求的,我很好奇,获取请求,它是怎么做到的,系统每秒少则几千,多则上万的请求并发,它获取的请求到底是哪个请求,是我想要的那个吗?
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return requestAttributes == null ? null :((ServletRequestAttributes)requestAttributes).getRequest();
}
跟着就可以看到一个请求上下文容器RequestContextHolder,getRequestAttributes获取了请求属性,这个请求属性不重要,反正不是我想要看到的,就看看这个容器里有啥吧
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
/**
* Return the RequestAttributes currently bound to the thread.
* @return the RequestAttributes currently bound to the thread,
* or {@code null} if none bound
*/
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
代码里有个稍微熟悉一点的类了,很熟悉但不完全熟悉。NamedThreadLocal和NamedInheritableThreadLocal
我以前也不知道这两个类是啥,我只知道ThreadLocal。
先看下ThreadLocal是啥吧
ThreadLocal看名字指的是线程本地的变量,也就是说由ThreadLocal保存的变量是属于当前线程的,这个变量对于其他变量是隔离的,是不透明的,ThreaLocal为每一个线程都保存了一份这个变量的副本,那么每个线程就可以访问自己线程的副本变量了,怎么感觉是在说废话呢?
来看下源码是怎么写的吧,源码中主要的两个方法就是set方法和get方法
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//从当前线程中获取线程变量ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//从map中根据key(也就是当前ThreadLocal对象)获取其对应的Entry节点
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//初始化当前线程的ThredLocalMap或者当前ThreadLocal对应的属性值
//这个方法和set()方法类似,但是这个方法的默认value值是null
return setInitialValue();
}
/**
*set()方法和setInitialValue()类似,但是set()方法是去设置ThreadLocal对应的值
*如果当前线程的ThreadLocalMap为null,将会先创建map再设置第一个值
*/
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和Entry是啥吧
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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);
}
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
}
ThreadLocalMap是ThreadLocal的静态内部类,而Entry是ThreadLocalMap的静态内部类(禁止套娃警告)
ThreadLocalMap的作用就是为了保存多个ThreadLocal变量,以ThreadLocal对象为key,属性值为value
从上面源码可以看到ThreadLocalMap中真正保存持有这些ThreadLocal变量的对象是一个Entry数组对象
看着ThreadLocal风风火火,其实它实际的用处就是hash出一个index值,这个index表示ThreadLocal属性在Entry数组中的位置
但是如果hash值一样的话,那不就产生hash冲突了吗?它是这样做的
采用线性探测的方法一个一个的探测当前位置是否存在Entry对象,如果有,就去比对ThreadLocal对象是否与Entry中取出的key一致。
如果一致就替换当前key对应的属性值;如果不一样,则调用replaceStaleEntry来设置值,同时会去清除那些key为空的value,以避免产生内存泄漏问题,也会将该Entry置为null,以备下次被使用
Entry中的键使用WeakReference修饰的,当ThreadLocal不再被使用时,将会及时被回收,但是Entry中value是强引用,这样的话Entry仍然会一直存在于内存中,及时该Entry对象已经形同虚设,所以Java8中对此作了优化,在ThreadLocal的get()、set()、remove()调用时,会去清空那些key为空但是value不为空的Entry对象,避免发生内存泄漏问题
每个ThreadLocal只能保存一个变量副本,如果想要一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
Thread还持有一个inheritableThreadLocals引用,有兴趣的同学可以自行研究下