这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
是什么
ThreadLocal是一个比较老的类了,确切地说,在JUC之前就已经存在了。
- 第一次出现是在JDK1.2的时候。
ThreadLocal提供的是一个 线程本地变量,作用可以如此描述:
同一个ThreadLocal对象,对于不同的线程,可见的变量始终是该线程上做出所有修改的最新结果,和其他线程的修改无关,优点类似与于线程的私有变量。
我们使用了全局的线程池,做了以下的demo来看ThreadLocal的特性:
public class ThreadLocalTest {
private static ThreadLocal<Integer> localI = new ThreadLocal<>();
static{
localI.set(1);
}
private static void func1(){
GlobalThreadPool.execute(()->{
System.out.println("func1:first:"+localI.get());
localI.set(0);
System.out.println("func1:set and sleep");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("func1:second:"+localI.get());
});
}
private static void func2(){
GlobalThreadPool.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("func2:first:"+localI.get());
localI.set(10000);
System.out.println("func2:second:"+localI.get());
});
}
public static void main(String[] args) {
func1();
func2();
}
}
复制代码
我们先让第一个方法初始化localI并初始化;随后第二个方法对Thread对象进行读写。
结果:
func1:first:null func1:set and sleep func1:second:0 func2:first:null func2:second:10000
此处可以看到:
- func2中的变量并没有受到func1中修改的影响。
因此,ThreadLocal可以用在一些需要线程提供隔离环境,但又需要公共变量统一维护的地方使用。
如何做到
“见人说人话,见鬼说鬼话”(根据不同的特征值获取不同的已存的对象),这种功能我们一般会联想到Map映射的相关功能。我们知道java中的线程是有一个唯一线程ID的,那么如果我们使用相同的思路:
- 使用线程ID作为key
- 将线程的数据作为value
那么我们是否可以做到相同的功能,每次存取都是独立的?
答案是肯定的,源码中的实现也是使用了类似的思路。
ThreadLocal源码相关设计
相关数据的维护 – 存取
- ThreadLocal内部实现了一个简单的定制版本hashMap,来作为存储线程局部变量的结构。
static class ThreadLocalMap { //....
}
复制代码
-
Thread内部包括了一个ThreadLocalMap的变量,来作为线程的本地存储。
public class Thread implements Runnable { //............ /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; } 复制代码
确切地说,ThreadLocal只是维护了线程自身的相关私有变量,本身并不负责存储该部分信息。
看我们使用最频繁的两个方法:set和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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
复制代码
数据存储单元
数据清理
-
源码在实现角度,考虑了后续处理的问题:
- 线程退出的时候,需要将线程的变量销毁,否则会占用不必要空间。
我们可以通过弱引用(weakReference)来进行数据退出时的销毁。
ThreadLocal的内部使用的存储单元 entry,继承并扩展了 WeakReference,来做到类似的功能:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
我们知道:
- weakReference在下一次全局GC时,如果没有被强引用的地方,那么会被清理掉
那么,一旦线程退出,就意味着该entry是无法再次被访问到了,那么自然而然会被JVM清理掉。
数据存储
在这部分我们对比HashMap中的Entry,可以看到此处的Entry并没有设置前后指针的设定,那么存储是如何进行的呢?我们看相关的get/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();
}
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);
}
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;
}
复制代码
在set中我们可以看到:如果发生了hash碰撞,Map会根据情况,做三个操作:
-
如果hash碰撞索引处的原来的entry:
-
key不为空且和我们需要写入的key不同
- 此时取下一个索引位置
-
key不为空,和我们写入的key相同
- 替换,返回
-
key为空,那么进行 replaceStaleEntry的操作,在这个地方源码会将对应的hash中的value清理掉,随后将我们要设置的value填上去。
没有注意到的地方
-
我们知道线程是需要考虑重入性的,ThreadLocal有相关API能够支持这个特性。
JDK1.2中考虑到了该情况,因此实现了一个子类ThreadLocal的子类InheritableThreadLocal,来提供这一特性。
写了一个如下的demo,来测试该类的重入性以及可见性:
new Thread(()->{
InheritableThreadLocal <Integer> inheritLocal = new InheritableThreadLocal();
inheritLocal.set(10);
System.out.println("func3:value"+inheritLocal.get());
new Thread(()-> {
System.out.println("func3-ChildThread1:value:" + inheritLocal.get());
inheritLocal.set(1000);
System.out.println("func3-ChildThread1:value:" + inheritLocal.get());
}).start();
new Thread(()-> {
System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("func3:value:" + inheritLocal.get());
inheritLocal.set(50);
System.out.println("func3:value:" + inheritLocal.get());
}).start();
复制代码
输出:
func3:value10
func3-ChildThread1:value:10
func3-ChildThread1:value:1000
func3-ChildThread2:value:10
func3-ChildThread2:value:10
func3:value:10
func3:value:50
func3-ChildThread2:value:10
复制代码
我们得出以下结论:
- 子线程可以获得在进入线程之前最新更新的值
- 在子线程开始之后,父子线程所获取的值就是独立的了,并不会相互影响,相当于线程开始时获取了一个最新的值作为初始值。
查看源码,可以看到InheritableThreadLocal内部重写了ThreadLocal的两个方法做了快照以及替换:
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
同样的,线程内部也维护了一个inheritableThreadLocals的变量。
ThreadLocal的内存泄露问题
内存泄漏:占用但无法回收
Thread中维护了一个ThreadLocalMap,key是弱引用。
引用ThreadLocal的强引用消失了,因为key是弱引用因此ThreadLocal会被回收。
会导致ThreadLocalMap中的key变成了null,但value被强引用指向了某个线程变量,value会一直回收不了。
-
为什么key使用弱引用?
- 如果是强引用,那么ThreadLocal持有了ThreadLocal的强引用,那么ThreadLocal无法回收。
- 使用弱引用,ThreadLocal也会被回收,在下一次ThreadLocalMap使用时就会被清除。
内存泄漏根源:
ThreadLocalMap维护在ThreadLocal,生命周期和Thread一样长;同时Thread
正确使用方法:
- 使用完ThreadLocal使用完调用remove方法进行清除。
- 或将ThreadLocal定义为private static,一直存在强引用,任何时候都可以通过ThreadLocal的弱引用key访问到entry的value值,进而清除。