原创/朱季谦
在登录模块里,当用户完成一次登录会话后,往往需要将其登录成功的信息进行缓存。不同的登录会话,属于不同的会话线程,彼此需要互不影响。这就意味着,登录成功的信息,只属于该次会话线程的本地变量,这时,就可以基于ThreadLocal缓存属于该会话线程的用户信息,类似线程的私有本地变量。
可以这样实现,先定义一个存储用户信息的POJO类——
@Data
public class User {
private String username;
private String role;
......
}
写一个专门缓存会话用户信息的类SessionCsche,在该类里定义一个static修饰的ThreadLocal变量,再定义一个将User信息缓存到ThreadLocal的方法,以及从ThreadLocal取出的方法——
public class SessionCsche {
//定义一个static修饰的ThreadLocal变量
private static ThreadLocal<User> threadCache = new ThreadLocal<>();
//缓存用户信息
public static void put(String username, String role) {
User user = new User();
user.setUsername(username);
user.setRole(role);
threadCache.set(user);
}
//读取缓存在该会话线程里的用户信息
public static String getUsername() {
User user = threadCache.get();
if (user == null) {
return null;
}
return user.getUsername();
}
}
以上,就是基于ThreadLocal实现一个线程隔离缓存变量的场景。除此之外,在DataSource数据源动态切换场景下,也可以基于ThreadLocal实现不同的线程获取到不同的DataSource数据源,进而发起一次数据库访问会话。
以上主要是常见的应用场景,在大概了解一下ThreadLocal的应用后,就可以阅读一下其内部的源码实现类。
先来看threadCache.set(user)内部实现——
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
该源码首先通过 Thread t = Thread.currentThread()获取到当前线程对象,再通过getMap(t)取出当前线程的ThreadLocalMap对象,如果获取到的map不为空,就直接将new ThreadLocal<>()创建的对象threadCache当作key,以需要缓冲的数据当作value,以key-value格式缓存至线程的ThreadLocalMap对象里。若当前线程的map为空,则表示当前线程还没有创建ThreadLocalMap对象,因此就需要通过createMap(t, value)完成ThreadLocalMap对象创建后,再做缓存。
简单看一下createMap(t, value)内部逻辑,可以看到,里面通过new ThreadLocalMap(this, firstValue)创建了一个ThreadLocalMap对象,然后将ThreadLocal对象本身和需要缓存的数据,当作参数传给该ThreadLocalMap的构造方法——
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
进入到 new ThreadLocalMap(this, firstValue)内部,主要逻辑就是创建一个存放hash值到table数组,长度INITIAL_CAPACITY默认是16,这与HashMap默认长度一致都是16,然后通过firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)对传进来的ThreadLocal对象当作key去做一次哈希计算,计算其在数组中的索引位置,然后在该位置创建新的Entry对象,用于存储键值对,这里的键是ThreadLocal对象,值是需要缓存的User用户数据。
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);
}
进入到Entry类,看一下其内部结构——
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可见,这里的关键地方,正是这一个当前线程里的ThreadLocalMap对象。
每一个线程都有只属于本线程的ThreadLocalMap对象,类似线程的本地变量,当前线程只能读取到自己的ThreadLocalMap对象缓存信息,使用到了一种用空间换时间的方案。
接着,看一下ThreadLocal读取缓存的原理——
User user = threadCache.get();
进入到get()内部,首先通过 Thread t = Thread.currentThread()获取当前线程对象,接着去取出当前Thread对象的ThreadLocalMap,若前面已经有数据缓存到ThreadLocalMap了,说明该map就是有值的。这时候,通过ThreadLocalMap.Entry e = map.getEntry(this),以当前ThreadLocal对象当作key,内部以 key.threadLocalHashCode & (table.length - 1)方式计算出该key的在ThreadLocalMap内部数组的哈希索引位置,进而取出该缓存数据赋值给e,如果e != null,说明取出的缓存数据有效,就可以强制转换相应的类型对象,进而返回。
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}