ThreadLocal 分析
首先我们看一下下面这个程序
public class ThreadLockDemo {
//初始tl per对象名是 zs
static ThreadLocal<per> tl = new ThreadLocal<per>() {
protected per initialValue() {
return new per("zs");
}
};
static class per{
String name;
public per(String n) {
this.name = n;
}
}
new Thread(()->{
try {
//线程0 在tl中放入了新的per对象 ls ,并将线程停留
tl.set(new per("ls"));
System.out.println(tl.get().name);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
//线程1 首先睡眠 1S
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//按照正常的逻辑,tl被 线程0 set 了 ls 对象,此时取出应该是 ls,而结果却是 zs 是tl设定的初始值
System.out.println(tl.get().name);
}).start();
}
}
zs
ls
然后我们在看下面这个程序,按照上面程序的理解,下面程序的输出结果应该是zs ls zs 但是结果却是 zs ls ls,证明了tl 里面的对象并不是在本地新建一个对象,而是在本地复制了对象的引用,然后线程1通过这个引用把对象修改了,所以线程0后面取到的数据就是修改后的了。
public class ThreadLockDemo {
static ThreadLocal<per> tl = new ThreadLocal<per>();
static class per{
String name;
public per(String n) {
this.name = n;
}
}
public static void main(String[] args) throws InterruptedException {
per p = new per("zs");
new Thread(()->{
try {
//在线程0中添加p对象
tl.set(p);
System.out.println(p.name); //输出此时p对象name
Thread.sleep(5000);
System.out.println(tl.get().name);//获取此时p对象name
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}).start();
new Thread(()->{
//同样添加p对象
tl.set(p);
per pp = tl.get();
//修改p对象
pp.name = "ls";
System.out.println(tl.get().name);
}).start();
}
}
zs
ls
ls
下面是第二个程序的分析图
下面是ThreadLoacl的引用关系,简单说明一下set和get过程
set
在Thread内部的ThreadLocalMao中添加一个key 指向 ThreadLocal,value指向 需要保存对象的 entey 对象,此时的key指向entry是虚引用,这样就能保证在执行remove的时候,3这条指向断开或者指向其他对象),2这个指向因为是虚引用,所以Entry可以被垃圾回收,不然的话这个Entry被new ThreadLocal引用,造成内存泄漏。当然,如果不执行remove,5这条指向不会断开,同样会造成 new per("ZS")这个地方内存泄漏。
补充: 除了2这条指向new ThreadLocal的引用,还有一个tl指向它的强引用,这样才能保证new ThreadLocal这个对象不被回收。
下面是源码分析
//ThreadLocal的set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//得到当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
/*
这是getMap方法,显然是返回了一个当前线程的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
*/
//如果map不是空的
if (map != null)
//将当前的value设置到map里面,此时的this是当前线程,
//所以这个map的key是当前线程,以第二个程序为例,value是一个 per对象的引用连接,这里可以在设置之前打印看一下,是一个对象的引用
map.set(this, value);
/*
这是set方法实现细节
private void set(ThreadLocal<?> key, Object value) {
因为此时map不为空,需要拿到当前Thread里面的ThreadLocalMap里面的enery数组
Entry[] tab = table;
int len = tab.length;
拿到当前key的hashCode(被重写了,线程使用了AtomicInteger类)并和len-1,len肯定是2的倍数,与len-1 进行与操作可以大幅度保证 i 与hashCode相同
int i = key.threadLocalHashCode & (len-1);
从当前hashCode的位置开始找位置放当前value
for (Entry e = tab[i];
如果不为空就一直找下去
e != null;
如果 i+1 < len nextIndex 返回 i+1 ,否则返回 0 ,作用:如果当前key有值,就一直往后找,到达最后还没有找到空值位置再从第一个开始找。不会找不到的,因为上一次循环结束如果没位置了就会扩容,保证下一个有空位置。
e = tab[i = nextIndex(i, len)]) {
下面过程要记得key 是 ThreadLocal 对象,不是 Thread 对象
ThreadLocal<?> k = e.get();
如果存在相同key则直接覆盖,这是第一个程序中tl.set(new per("ls")) ls 覆盖 zs 处理部分
结束循环
if (k == key) {
e.value = value;
return;
}
如果当前ThreadLocal不存在这个key,结束循环
if (k == null) {
从当前位置开始将脏的Entry都处理掉
replaceStaleEntry(key, value, i);
return;
}
}
把当前元素的值给到Entry[i]
tab[i] = new Entry(key, value);
大小加1
int sz = ++size;
如果清理了一部分脏数据就判断是否需要扩容,否则判断一下
if (!cleanSomeSlots(i, sz) && sz >= threshold)
扩容,这个依旧是先判断有没有脏数据,如果清理成功就不扩容
rehash();
}
*/
else
//创建一个新的ThreadLocalMap
createMap(t, value);
/*
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
INITIAL_CAPACITY = 16
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
设置上限为16 * 2/3
setThreshold(INITIAL_CAPACITY);
}
*/
}
下面考虑最后一个问题,假设同时又两个线程执行 tl.set()
解答:
执行 tl.set 首先是调用 ThreadLocal 内部的 set 方法,内部是不同的线程的各自的ThreadLocalMap对象的增减,彼此线程之间不影响,所以可以多线程使用,这部分是安全的。虽然是同一个ThreadLocal