之前所学不精,现在看一下确实是,我ThreadLocal里如果都存的是一个共享变量的话,那么肯定是会两边都相同的。其实现在回头看这些代码就没有了当初学术不精时候的疑惑了,反正也被喷了,趁这个被喷的时间索性更正一下ThreadLocal的存储机制。
测试代码相当简单
public static void main(String[] args){
ThreadLocal<String> tl1 = new ThreadLocal<>();
tl1.set("tl1");
System.out.println(tl1.get());
}
这里要分析的也就两行
- ThreadLocal是怎么set的
- ThreadLocal是怎么get的
ThreadLocal是怎么set的
我们直接就看ThreadLocal的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方法是什么
- map.set方法是怎么set的
- createMap方法是什么,为什么需要线程参数 t
其实这里对于第四个点,我没点进去看也是有点疑惑的,为什么这个createMap方法的两个参数和上一句map.set的两个参数不一样,这不都是set一个键值对么,然后点进去就什么都知道了。(我之所以这么说是因为我觉得总会有人和我想的一样的)
下面逐一解释这4个点
ThreadLocalMap类是个什么东西:
ThreadLocalMap是ThreadLocal的一个静态内部类,内部指的看一下的东西如下
- Entry类,这个类比HashMap里的Entry简单多了,就一个构造,参数一个是Threadlocal键对象,一个Object值对象
- Entry数组table,做hash存储用的,懂HashMap的我就不说了
- 再就是阈值啊,初始大小之类的参数,这些在此文章就不关心了
getMap方法是什么:
getMap(Thread t)方法也是ThreadLocal类的一个内部方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这方法的意思就是返回线程t的内部参数threadLocals,关于线程对象中threadLocals参数,总结起来就是你用不到ThreadLocal,线程对象的这个属性就一直是null,这一点了解到这里就可以了,有兴趣可以去看Thread类。接着上面的逻辑,如果getMap不是空,就用ThreadLocalMap的set方法置入一个以当前ThreadLocal对象为键,value为值得这么一个键值对;如果getMap为空,那么就以createMap方法set第一个值。
map.set方法是怎么set的:
ThreadLocalMap类的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();
}
这里的处理逻辑几乎和HashMap的一样,虽然没HashMap那么细
- 计算当前键的hash值
- 去table里找,重复键就替换值,不重复就在该位置添加这个键值对
- 当前容量超过阈值就扩容然后rehash()
createMap方法是什么,为什么需要线程参数 t:
关于createMap方法的逻辑
void createMap(Thread t, T firstValue) {
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);
}
createMap方法的意思就是,构造一个新的ThreadLocalMap对象,将value对象塞进map,然后把传入的线程对象的threadLocals属性指向这个新ThreadLocalMap。
ThreadLocal是怎么get的
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
和HashMap一样,对应的key有就返回value,没有就null
到这里代码就讲解完了
总结(虽然我很想把总结写在开头)
- ThreadLocal进行set的时候,是在当前线程Thread中获取到有且唯一的ThreadLocalMap对象(如果没有就新建一个ThreadLocalMap对象设置进Thread的属性里),然后把自己作为键,value作为值set进这个Map里
- ThreadLocal进行get的时候,是从当前线程Thread中获取到有且唯一的ThreadLocalMap对象(Thread的ThreadLocalMap属性如果为空,也就是说这个线程从来都没有用过ThreadLocal设置过值,返回null),然后把自己做为键去该Map里面找,找到就返回对于的value,没有就返回null
昨天查资料看到了ThreadLocal这个类,原来一直没有仔细关注过,牛客网看到的一道题说
ThreadLocal用哈希表的形式为每一个线程都提供一个变量的副本
并且给的回答是正确的,这里我们想一下,什么叫变量的副本,如果某一个线程中副本被修改,那么,其他线程中“副本”会不会被修改。
我们来看以下代码:
public class Demo1 {
private static ThreadLocal<Student> local = new ThreadLocal<Student>();
public static void main(String[] args) {
final Student student = new Student(); //所谓的副本原始对象,我们就存这个
student.setAge(19); //给个初始值19
/**
* 实验策略是创建两个线程都进行保存student,然后都休息一段时间(给个3秒)
* A线程休息完后修改student中的年龄为11
* B线程在休息完3秒后继续休息2秒,目的是为了等A修改完
* B线程休息完后取出自己所存的Student,看看里面的age到底是19还是11
*/
new Thread(){ //A线程
public void run() {
local.set(student);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
local.get().setAge(11);
};
}.start();
new Thread(){ //B线程
public void run() {
local.set(student);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(local.get().getAge());
};
}.start();
}
}
class Student{
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
最后结果为11,这个结果也就是说两个线程里面存的是同一个Student对象,修改时线程之间会被影响,而不是所谓的各自一个“副本”,谁也影响不了谁
具体ThreadLocal中是怎么存的,简单来说就是ThreadLocal类有方法调用当前Thread的ThreadMap对象(该对象不是HashMap的子类,但是同样实现了HashMap中的拉链式的结构,并且是Thread的内部类),拿到对象后把自己(ThreadLocal)当键,在里面找有没有已经存在的自己,也就是判断自己是否以前存过东西,存过就替换值,没存过就新开辟地方存值。
对于ThreadLocal的具体源码解析,博主http://blog.csdn.net/wanzaixiaoxinjiayou/article/details/49703135有具体分析。