最近开发过程中有一个新功能,让一个web应用供登录用户通过浏览器访问,后台应用会获取用户的登录信息(如用户名),并对每个用户的访问做记录. 这是一个并发场景,每次请求都分配一个线程去处理这个请求,web容器一般都会有一个线程池,每次请求都会分配其中的一个空闲线程去处理用户的这次请求, 处理完毕后,线程归还线程池等待后续访问的线程分配.
当然,用户登录信息可以从当前请求request中获取,但是后台应用的多个地方可能都会需要用户登录信息, 一个解决办法是向这些所有用到的地方传递request参数,显然是麻烦的。另外一个办法就是利用ThreadLocal, 获取登录信息后把它放到当前线程中的ThradLocal变量中,任何需要的时候从当前线程中取就可以了,是不是很方便呢?
ThreadLocal介绍
ThreadLocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,这个变量于当前线程所独有,并且线程之间不会发生冲突。
类中定义的ThreadLocal实例一般会被private static修饰,这样可以让ThreadLocal实例的状态和Thread绑定在一起,业务上,一般用ThreadLocal包装一些业务ID(user ID或事务ID)——不同的线程使用的ID是不相同的。
下面我们从一个例子开始,循着源码,走入ThreadLocal的原理世界吧!(作者目前使用的JDK版本是1.8.0_144版)
使用示例
public class ThreadLocalTest {
public static void main(String[] args) {
Bank bank = new Bank();
Transfor transfor = new Transfor(bank);
Thread t1 = new Thread(transfor);
Thread t2 = new Thread(transfor);
t1.start();
t2.start();
System.out.println(bank.get());
}
}
class Bank {
// 2 ways to override the initialValue() method
private ThreadLocal<Integer> money = ThreadLocal.withInitial(() -> 100);
/*
ThreadLocal<Integer> money = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 100;
}
};
*/
public int get() {
return money.get();
}
public void set() {
money.set(money.get() + 10);
}
}
class Transfor implements Runnable {
private Bank b;
public Transfor(Bank bank) {
b = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
b.set();
System.out.println(Thread.currentThread() + " ---- " + b.get());
}
}
}
从以上代码,我们可以看到:我创建了一个Bank类,并重载initialValue()方法使money=100;Transfor类实现Runnable接口,并由main方法生成两个子线程进行调用。
猜猜打印出来的结果是什么?
Thread[Thread-0,5,main] ---- 110
Thread[Thread-0,5,main] ---- 120
Thread[Thread-0,5,main] ---- 130
Thread[Thread-0,5,main] ---- 140
Thread[Thread-0,5,main] ---- 150
Thread[Thread-0,5,main] ---- 160
100
Thread[Thread-1,5,main] ---- 110
Thread[Thread-0,5,main] ---- 170
Thread[Thread-1,5,main] ---- 120
Thread[Thread-0,5,main] ---- 180
Thread[Thread-1,5,main] ---- 130
Thread[Thread-0,5,main] ---- 190
Thread[Thread-0,5,main] ---- 200
Thread[Thread-1,5,main] ---- 140
Thread[Thread-1,5,main] ---- 150
Thread[Thread-1,5,main] ---- 160
Thread[Thread-1,5,main] ---- 170
Thread[Thread-1,5,main] ---- 180
Thread[Thread-1,5,main] ---- 190
Thread[Thread-1,5,main] ---- 200
机智的同学们,发现规律了么?ThreadLocal为money这个变量在每个线程中都生成了一个完全相互独立的值,在对这个值做任何操作的时候,线程之间不会相互影响。这样的特性能帮我们完成很多特殊的业务需求,同时让代码更加简洁且易于维护。想知道ThreadLocal怎样实现如此有趣的功能吗?请继续往下看:
源码分析
先上一张ThreadLocal内部结构图:
发现了什么规律了么?ThreadLocalMap是这个class的存储核心!
- 每个Thread线程内部都有一个Map。
- Map里面存储线程本地对象(key)和线程的变量副本(value)。
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。同一个Thread中多个ThreadLocal对象共用一个Map。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
那么首先我们来了解一下ThreadLocalMap相关知识吧!
ThreadLocalMap
此变量在Thread类中被定义:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
而在ThreadLocal类中被初始化/调用
public class ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
可以看到,Map中key存的是当前ThreadLocal对象,而value是我们需要保存的值。
ThreadLocal公有方法
ThreadLocal提供的公有方法如下:
这些方法都围绕着ThreadLocalMap进行操作
1.withInitial()
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
Java 8以后用于初始化ThreadLocal的一种方法,在外部调用get()方法的时候,会通过Supplier确定变量的初始值;(相当于在new一个ThreadLocal对象时override initialValue()方法)
2. set()
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 getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
步骤:
1.获取当前线程的成员变量map
2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
3. get()
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
4. remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.根据ThreadLocal对象删除这个map中对应的Entity
至 此 , T h r e a d L o c a l 的 基 本 原 理 已 经 讲 完 了 。 如 果 你 还 想 了 解 更 多 , 请 继 续 向 下 看 . . . \color{#ea4335}{至此,ThreadLocal的基本原理已经讲完了。如果你还想了解更多,请继续向下看...} 至此,ThreadLocal的基本原理已经讲完了。如果你还想了解更多,请继续向下看...
Hash冲突
单独将Hash冲突提出来讲,是因为ThreadLocalMap解决冲突的方式与HashMap不同。众所周知,HashMap使用链地址法解决冲突,而ThreadLocalMap使用的是线性探测法。
线性探测:就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
为何出现
ThreadLocalMap为一个初始能够保存16个元素的map,每当已使用的空间占整个储存空间的2/3时(2/3为默认临界值),将会将整个Map的容量扩大一倍,并重新计算各个元素的Hash值,存入相应的地方。我们可以设想一下,存储位置都是由每个key的hashcode计算而来,并且希望存到2/3的时候才会扩容,那么一个Thread内的多个ThreadLocal对象之间就容易出现多个对象希望存入同一个空间的冲突,此时就需要解决此问题,让每个Map元素都有位置能存入。
/**
* 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;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
如何解决
ThreadLocal引入了一个魔法HashCode: 0x61c88647来尽可能的减少hash冲突,nextHashCode()会使用到此HashCode。在每次生成一个新的ThreadLocal对象时,都会根据nextHashCode生成一个int值作为哈希值,然后根据这个哈希值&(数组长度-1),从而获取到哈希值的低N位(以len为16,16-1保证低四位都是1,从而获取哈希值本身的低四位值),从而获取到在数组中的索引位置。如果还是冲突了怎么办?请看以下ThreadLocalMap的set()源码。为了帮助理解,我加入了一些注释,能够看出使用的是线性探测法,且步长为1。
private void set(ThreadLocal<?> key, Object value) {
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();
// 如果是同一个对象,则覆盖value值
if (k == key) {
e.value = value;
return;
}
// 如果key为null,则替换它的位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
// 否则继续向后线性探测,直到找到空位为止
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
是不是很奇怪为何要用这个魔法数字?因为这个魔法数字能将HashCode计算出来的int型hash值尽可能的均匀分布。
想知道这个魔法数字怎么来的吗?简单来说,是2的32次方乘以黄金比例(√5-1)÷2近似0.618。
关于魔法数字的更多细节,请参考博文 Why 0x61c88647?