最近面试关于ThreadLocal的问题竟被一面和二面的面试官同时问了。问怎么实现的?以前都是知道怎么用,没看过源码。所以没回答上来,感觉在这种低级的问题上丢分很不值当,所以抽空看了一下ThreadLocal的源码。记录下来,加深印象。言归正传。
ThreadLocal 即线程本地变量。即每个线程持有一个变量的副本,线程对变量的操作只针对于变量值的副本。ThreadLocal和同步锁都是用来实现多个线程对同一个变量的安全访问。用同步锁机制的时候,变量只有一个,只不过是通过锁的作用让多个线程串行的去访问变量。Threadlocal是从另一个角度来解决多线程下并发访问变量不安全的问题,ThreadLocal将需要并发访问的资源复制出多份来,每个线程都拥有自己独立的副本,从而就不存在不安全访问的问题了。
ThreadLocal并不能代替同步锁,俩者面向问题的领域不同。同步锁是为了多个线程对于资源的并发访问做的控制,资源对每个线程而言是同步。而ThreadLocal是复制多份资源,资源在每个线程中是不同步。
通常如果需要进行多个线程之间共享资源,以达到线程之间通信的功能,就是用同步锁;如果仅仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal。
简单的demo:
public class Test5 {
public static void main(String[] args) {
//创建一个ThreadLocal对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
//赋值
threadLocal.set("Hello");
//获取
String str = threadLocal.get();
//输出Hello
System.out.println(str);
//移除
threadLocal.remove();
//再次获取
str = threadLocal.get();
//输出null
System.out.println(str);
}
}
以下代码可以创建一个包含初始化值的ThreadLocal对象
ThreadLocal<String> threadLocal = new ThreadLocal(){
@Override
public String initialValue(){
return "defalut";
}
};
可以看到ThreadLocal核心就三个方法:
T get()
void set(T value)
void remove()
下面就这三个方法我们一起看看源代码,也没啥神秘的,不知道为什么面试官爱问!!!!
只看set方法源码即可!set看懂了get和remove基本不用看了!
//ThreadLocal.java
//代码块1
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到在Thread类有一个名为:threadLocals,类型为:java.lang.ThreadLocal.ThreadLocalMap类型的成员变量。申明如下:
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
【代码块1】第一次set的时候threadLocals为null,所以会走else ,下面看看createMap(t, value)方法里都干了什么?
t.threadLocals = new ThreadLocalMap(this, firstValue);
可以看到就是new 了一个ThreadLocalMap 赋给 Thread对象的threadLocals变量。ThreadLocalMap如下:
//ThreadLocal.ThreadLocalMap.java
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
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);
}
到这儿基本上能看到ThreadLocal的存储形式了!在捋一下,在Thread类有个名为threadLocals,类型为ThreadLocalMap类型的成员变量,ThreadLocalMap内部有个Entry数组,Entry对象的key是ThreaLocal自己,value则是往ThreadLocal里存的对象。
开始new出一个大小为16的Entry[],根据ThreadLocal对象的threadLocalHashCode和Entry[]大小计算entry对象在Entry[]中的存储下标。最后一步则是设置Entry[]数组扩容的阀值。阀值的意思是:Entry[]的实际使用率超过Entry[]长度的2/3的时候重新rehash扩容。
下面需要重点关注以下这个Entry对象,看源码:
//ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
可以看到Entry 继承了 WeakReference对象,关于WeakReference(弱引用)不了解的可以看看博主的另一篇文章《五分钟搞明白JAVA的软引用,弱引用,虚引用》,那么继承WeakReference后Entry会有什么样的特性呢?为什么要继承WeakReference呢?请注意记住下面标红的话,后面说ThreadLocal会不会有内存溢出的问题时候会用到。当ThreadLocal对象失去引用强应用的时候,Entry.get() 会返回null,但是Entry.value还被Entry对象引用的。因为Entry继承WeakReference,所以Entry所引用ThreadLocal当失去强引用的后,会被GC回收掉。所以Entry.get ()会返回null。
以上代码都是第一次调用ThreaLocal.set的时候进入else分支的时候,那现在看看第二次及以后调用ThreaLocal.set方法,即Thread.threadLocals 不等于null的时候,即进入(代码块1)的if分支。有是怎么做的呢,看源码:
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//tag-1
int i = key.threadLocalHashCode & (len-1);
//tag-2
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//tag-3
if (k == key) {
e.value = value;
return;
}
//tag-5
if (k == null) {
//tag-5
replaceStaleEntry(key, value, i);
return;
}
}
//tag-6
tab[i] = new Entry(key, value);
int sz = ++size;
//tag-7
if (!cleanSomeSlots(i, sz) && sz >= threshold){
//tag-8
rehash();
}
}
这段代码可能看着比较绕,别急!我们一行一行的来分析。
未完待续!太晚了。。。
---------------------------继续下班没事干了继续写写(2018.08.14 19:00)
tag-1计算Entry[] 数组的下标,tag-2的处循环判断e是否为空,为空就表示Entry[i]是为被使用的,结束循环到tag-6 new 处一个Entry对象填充到Entry[i]的位置,紧接着++size。这里的size表示Entry[]的实际使用个数,即在当前线程上一共new 出了多个少ThreadLocal对象。tag-7处的 cleanSomeSlots方法是干什么用的呢?看代码:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
其实根据名字就能判断出来这个一个释放已经失去引用的Entry对象。如果成功释放了部分Entry对象就返回true,没有Entry被释放就返回false。tag-7处判断:如果没有成功释放调Entry对象 且 size 大于 扩容阀值 threshold,则执行tag-8,扩容Entry[]数组。
接下来看看什么时候会进入tag-3处的if 体呢?即重复调用 ThreadLocal.set方法的时候,因为这个时候对应的Entry对象已经创建,重复调用set只是在不停的替换Entry的value属性而已。
那么什么时候会进入tag-5处的if 体呢?当另一个ThreadLocal对象失去强引用的时候,ThreadLocal对象被GC掉了,对应的Entry对象的get方法会返回null。所以if体里用当前的ThreadLocal和value生成新的Entry对象替换Entry[i]处失去引用的ThreadLocal对象对应的Entry对象,从而让已经失去引用的ThreadLocal对象引用的value失去引用。可以GC回收掉过期的Value,防止内存溢出和泄漏。
这里大家要理解,当new 一个ThreadLocal对象,调用set方法后会生成一个Entry对象存放到一个Entry[]数组中。当这个ThreadLocal失去强引用后,ThreadLocal对象会被GC掉,Entry.get 方法会返回null,但是对应的Entry对象还是被Entry[] 所引用,并不会被GC的。那么什么时候这个Entry会被GC呢?三种情况:
1.当前线程运行结束,即Thread对象失去引用(要注意tomact,jetty,线程池一般不会轻易的销毁一个线程)。
2.主动调用ThreadLocal对象的remove方法。
3.在同一个线程上调用别的ThreadLocal的set方法自动清理失去强引用的ThreadLocal对象关联的Entry对象。
看看下面代码的
public static void main(String[] args) {
Thread thread_dmeo = new Thread(new Runnable() {
@Override
public void run() {
ThreadLocal<String> threadLocal_s = new ThreadLocal<>();
threadLocal_s.set("hello");
}
});
thread_dmeo.start();
}
大概内存引用如上图,Thread对象引用着一个Entry[]数组,Entry保持着对ThreadLocal对象弱引用。thread_demo线程死亡,则所有相关对象都会释放。若threadlocal_s失去对ThreadLocal对象的引用后,GC会回收ThreadLocal对象,下次往Entry[]添加Entry对象的时候,会自动遍历Entry[]数组,调用Entry.get()方法返回null, 系统就会断开Entry与“Hello”的强引用。故下次GC的时候“Hello”会被回收掉。
故如果往ThreadLocal里set对象后,一直没有调用remove方法,会不会有内存溢出和泄漏的隐患呢?
个人认为不会有内存溢出的问题,泄漏更谈不上了。因为ThreadLocal每次set会自动回收失去引用相关对象。但是在使用线程池或Tomcat等web容器的时候,因为线程复用的原因造成获取保存在ThreadLocal中的数据时出现脏读的问题。但这不是ThreadLocal本身的问题。因为如果不及时调用remove方法释放的资源的话,会有内存回收不及时,脏读等隐患,所以还是建议使用后养成remove的习惯。