多线程
多线程编程中往往可能需要专门为一个线程维护独有的数据,例如一场考试中,有100个人在做题,每个人虽然都是同一个试卷,但是作答的结果肯定有不同,那么就需要有一个线程隔离的变量保存每个人的答案。首先看Thread源码。
Thread
Thread的源码中有一个成员变量是ThreadLocalMap类型,这个类型是ThreadLocal类型的内部静态类。我们先通过名字猜测一下是干什么的,首先ThreadLocal就是"线程+本地"两个词组合在一起,那么很容易想到就是线程自身属性的一些东西,加个s就是一堆线程自身属性的东西,换句话来说就是当前线专属自己的变量们,为什么加个“们”,往后看就知道了。
ThreadLocal
这个就是我们的核心类,当我们需要保存当前线程专属的变量时,就申请一个这个东西。举个例子,有个人来到了公司,进门的时候通过人脸识别身份认证,挂上工牌就知道他是A部门经理,然后他去B部门的档案室,那里的门卫看看他的牌子(没有人脸识别身份认证)说:你是A部门的,不能进B部门档案室。这里,这个人就是一个线程,进门人脸认证就是鉴权,然后认证成功后,挂牌的过程就是将认证信息放到这个线程的threadLocals中(这个通过ThreadLocal的set方法实现),等到要进入B部门的时候,查看牌子就是ThreadLocal.get就不用通过人脸认证了。
现在我们重点关注ThreadLocal的get和set方法,至于Thread中的threadLocals变量可以暂时理解为这个人身上除了这个牌子,还有手机、电脑等等其他专属于他的物品。
在关注set方法之前,我们得先new一个出来,不new出来怎么set嘞。new的话我们就用下面的
class Person extends Thread{
// job意思就是这个人的职位
private ThreadLocal<String> job= new ThreadLocal<>();
}
// ThreadLocal源码
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 根据当前线程对象,取得当前线程对象的threadLocals(这是个map)
// getMap(t) 就是 t.threadLocals;
ThreadLocalMap map = getMap(t);
if (map != null) {
// 给map中添加元素
map.set(this, value);
} else {
// 如果是空的map就重新创建一个
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
// 用构造函数的方式创建新的map
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLocalMap源码
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);
}
首先关注set方法,他做的第一步就是获取当前的线程对象,因为存储当前线程独有的数据肯定得先获取线程,然后将数据存到threadLocals里面。因此首先做的就是get到当前线程对象的threadLocals成员变量,如果是空的,那就new一个并且把值作为第一个存进去,如果不是空的就添加一个。这个threadLocals实际上是一个map,为什么是map呢,因为当前的线程可以拥有好多个独有的变量,以key-value的形式存储在当前线程对象的threadLocals成员变量中。其中这个key呢就是this,这个this是ThreadLocal对象的this,也就是上面那个job的this(它的hashCode),value是我们想要的值,这样以后想要的时候,使用job.get(),在get方法里先获取到当前线程的threadLocals,然后以job自身(它的hashCode)作为this,去map中取值。
为什么是hashCode呢,我们去map.set方法中看一看就知道了,其实这个key就是hashCode取了一下余。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 剩余省略
}
下面我们再看一下get方法,理解了set方法之后get方法就更简单了。
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
// 把this(hashCode)作为key取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
最后我们用一个例子来说明这个过程。通过这个例子就可以明白为什么需要用map来存数据,而且还要用ThreadLocal的hashCode做key,这都是因为一个Thread可以有多个专属变量。例如我们这个人可以有自己的电脑,也可以有自己的职位。将数据存在当前线程的threadLocals变量中是为了隔离,用map是为了job在get的时候不会错误的取到laptop。
class Person extends Thread {
public ThreadLocal<String> job = new ThreadLocal<>();
ThreadLocal<String> laptop = new ThreadLocal<>();
@Override
// 从run开始,这个人从家里出发去上班
public void run() {
// 打包好电脑
laptop.set("昨天买的笔记本");
// 到公司门口人脸识别,用sleep模拟
try {
sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 识别成功,他是A部门经理
job.set("A部门经理");
// 到工位取出电脑
System.out.println("今天带的笔记本电脑是:" + laptop.get());
// 去B部门档案室
System.out.println("你的职位是:" + job.get() + ", 不能进入");
}
}
public class TestThreadLocal {
public static void main(String[] args) {
Person person = new Person();
// 这里这个人的职务是B部门经理,为什么输出的是A部门经理?
person.job.set("B部门经理");
person.start();
}
}
在创建person的时候给他的职位是B部门经理,然后我们启动线程,发现输出的是A部门经理,这是因为,在创建person的时候是一个线程,而启动这个线程又开了一个新的,两个线程不在一起,因此不一致。