博客主要介绍关于java ThreadLocal的使用和源码理解,以及它是如何实现线程安全和变量隔离的。
一、结构:
(这里没有画图,将就到看哈)
(1).ThreadLocal跟很多集合一样是一个泛型类
(2).Thread包含一个ThreadLocal.ThreadLocalMap的成员变量(组合关系),也就是每个线程维护的独立副本的变量。
(3).ThreadLocal包含一个内部类ThreadLocalMap,ThreadLocalMap又包含一个内部类Entry,Entry最终就是给当前线程存储变量数据的。Entry的key存储ThreadLocal的弱引用,value指向Object对象值。
二、示例:
public static void main(String[] args) {
final ThreadLocal<Integer> local = new ThreadLocal<Integer>();
new Thread() {
@Override
public void run() {
local.set(10);
System.out.println("ThreadName: " + Thread.currentThread().getName() + "; value: " + local.get());
}
}.start();
new Thread() {
@Override
public void run() {
local.set(20);
System.out.println("ThreadName: " + Thread.currentThread().getName() + "; value: " + local.get());
}
}.start();
}
当前执行的线程中,Entry的key存储的就是local(ThreadLocal实例对象),而value就是当前线程执行set方法时传入的值,线程在执行get方法时最终就会拿到自己set的值,从而做到线程安全。
三、核心API理解
3.1 set方法
在讲set方法之前,先来看下ThreadLocal的构造方法
public ThreadLocal() {
}
什么鬼,啥也没干。好吧,就是这么简单。
上正菜吧!!
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
显而易见,这个方法就做了下面几件事:
1.首先获取当前线程对象
2.通过当前线程对象获取ThreadLocalMap对象
3.如果map==null,就调用createMap方法。
4.如果map!=mull,调用set方法
先来看看简单的getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
该方法直接调用当前执行的线程的threadLocals成员变量(即:ThreadLocal.ThreadLocalMap类型变量),但是Thread类并没有对该变量初始化,也就是说线程在第一次调用set方法时,map是为空的。
但是博主有个疑问:在测试过程中发现,主线程在启动初始化的过程中会有各种值被set方法传入到Entry中,而且获取到的map并不为空,而当创建的子线程第一次执行到set方法时map才等于空。望知道的大神门可以给我解释下啊!博主感激不尽。
接下来看看map==null的情况
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createMap就是给Thread的成员变量threadLocals赋初始值,并传入当前ThreadLocal对象和value值
再看ThreadLocalMap的构造方法:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
1.创建了一个Entry数组,初始化大小为16。
2.根据firstKey来计算该变量存储在Entry数组变量table的下标位置。(具体的计算原理请参考:http://www.cnblogs.com/ilellen/p/4135266.html)
3.赋值到table中下标位置为i的地方。
4.size=1设置数组的大小。
5.设置阈值,即:当Entry数组table大于INITIAL_CAPACITY(16) * 2/3的时候就需要对table进行扩容。
到这里ThreadLocalMap构造方法初始化结束了。
接下来就是map!=null情况下调用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();
}
1.首先根据方法传入的key进行hash计算数组下标的值,根据这个值获取该数组的位置的元素e,判断e是否为空
2.当元素e为空的时候,也就是没有出现hash冲突,直接创建Entry(key, value)赋值给tab[i]。
3.当元素e不为空的时候,即出现hash冲突,进入for循环,通过e.get()获取当前元素的ThreadLocal对象引用k。如果k等于key,也就是tab中已经存在这个值,直接将其覆盖重新赋值。如果k等于空,但是此时e不等于空,就意味着value不为空,这里出现了陈旧元素(即:因为key是ThreadLocal的弱引用,如果这个对象没有被其他地方引用,就有可能被jvmGC回收了,而value不是),所以这里要对进行remove操作,并且要重新整理Entry数组。而这个replaceStaleEntry方法就是做这件事的,replaceStaleEntry方法较为复杂难以理解,有兴趣的开发者可以自行查看源码。但有一点还是说,给方法里面调用了expungeStaleEntry方法,这个方法就做一件事就是删除陈旧的元素。
4.最后看下倒数第二和第三行代码,调用cleanSomeSlots方法,这个方法就是去找Entry数组中是否有陈旧元素,如果有则将其删除并返回true,然后调用rehash方法对Entry数组进行扩容,重新调整Entry数组;如果没有找到则继续判断数组大小是否超过阈值了,如果超过阈值,也调用rehash方法调整Entry数组;否则不做任何操作。
至此,set方法粗略的逻辑分析完毕,给大家整理下主要的逻辑图
3.2 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();
}
get方法体前两行和set方法一样,接下来分2种情况:
1.map为空,直接调用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;
}
setInitialValue方法体第一行调用initialValue方法(如下面代码所示),这个方法直接返回null赋值给value;接下来还是获取map,此时map是为空的,所以调用creatMap方法创建新的ThreadLocalMap对象。最后直接返回null。
protected T initialValue() {
return null;
}
2.map不为空,通过map调用getEntry方法,传入的参数是当前ThreadLocal对象,getEntry方法如下:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
首先通过keyhash计算获取数组下标再拿到Entry元素e,判断e不等于null而且e元素的key等于当前传入的ThreadLocal对象,直接返回e;否则执行getEntryAfterMiss方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这个getEntryAfterMiss方法个人认为做了三件事:
1.通过while循环解决hash冲突
2.判断是否有陈旧元素,有的话将其删除
3.拿到最终元素e返回
最后,get方法拿到元素e后,调用e.value获取value值返回。至此,get方法分析结束
3.3remove方法
先上源码:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
相当于执行ThreadLocalMap的remove方法:
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
for循环是为了解决hash冲突,解决hash冲突就是将i+1,如果e元素的key等于当前传入的ThreadLocal对象,那么就清空这个元素。结束
四、总结
set、get、remove方法体都在开头获取当前线程对象来作为操作的对象,跟其他任何线程都无关,从而做到了线程安全和变量的隔离
虽然使用起来很简单,但是我们还是要注意一些细节问题,比如用线程池使用ThreadLocal时,虽然线程使用完了,但是该线程如果没有没有调用remove方法,并且该线程在线程池继续处于活跃状态的话,会出现两个隐藏的致命问题:
1.没有调用remove方法意味着没有把使用过的线程本地变量删除,因为线程的成员变量threadLocals可能还存储着上一次使用的对象,下次再使用这个线程资源的话,就有可能出现线程安全的问题
2.根据1的情况就有可能引发内存泄漏,最终导致严重的后果。
所以切记,在使用ThreadLocal以后一定要记得手动调用remove方法。
参考资料:
https://blog.csdn.net/wlwh90/article/details/90474230
仅以此博献给我伟大的java语言,如有不当之处,欢迎大神指正。谢谢!!!