一、ThreadLocal 线程私有的。
为什么说ThreadLocal是线程私有的? 上源码 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);
}
从源码得知。我们在往 ThreadLocal 中存数据时。 它首先 获取到当前线程。并从当前线程中拿到 ThreadLocalMap 。再往里存放数据。
接下来我们再来看ThreadLocalMap 中 key ,value 究竟又存放的是什么。显然 ThreadLocalMap 中 key ->ThreadLocal ;value ->我们set 进去的 value。
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();
}
由源码,我们很自然就能得出结论。 既然调用 ThreadLocal .set() 方法时,每次都是获取当前线程,再完成数据存储动作的。那么,它自然 具备了线程隔离性。由某一个线程所持有。所以ThreadLocal是线程私有的。
接下来,我们用代码来证明
/**
* 首先 我先初始化一个 threadLocal 。、
* 再主线程main 中 新启一个线程 thread,并在该线程中完成 set操作。
* 由此 当前应用存在两个线程 一个是 main ,一个是 thread
* 由于threadLocal 是线程私有的。所以 主线程 main 获取的值为 null
*/
public class Test {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
Thread thread = new Thread(()->{
System.out.println(String.format("往 线程%s 中 存入 1 ",Thread.currentThread().getName()));
threadLocal.set(1);
try{
Thread.sleep(1000);
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
}catch(Exception ex){
ex.printStackTrace();
}
});
thread.start();
try{
// 让 主线程睡一会儿。确保 cpu 已执行 thread 中的 run()方法
Thread.sleep(500);
}catch(Exception ex){
ex.printStackTrace();
}
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
}
}
验证完毕。让我们继续来看下源码。
1.1 首先,我们来看一下 Thread 类。java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals
/*java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals */
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
从上图我们可以看出ThreadLocalMap 是 ThreadLocal 中的 静态内部类。
1.2 接着我们来看下 ThreadLocal 中的源码。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
看过源码后我们发现 ThreadLocalMap 中 也包含一个 静态内部类 Entry 继承自 WeakReference<ThreadLocal<?>> 同时拥有 一个ThreadLocal 的引用。和 value。
那么问题来了。此处为什么要用 弱引用?
首先。我们再来复习一下,弱引用的特点。 当一个对象没有被强引用存在时。 弱引用 将被jvm忽视。直接gc掉。想一下,如果不是 弱引用。而是强引用,会有什么问题。假使 Entry 未继承弱引用,则 entry 对 ThreadLocal 强引用。 则 当 ThreadLocal 被复制为null时 意味着,ThreadLocal 已经没有用了。 但 entry 对 ThreadLocal 的 强引用。导致 ThreadLocal 没有办法被回收。 会造成内存泄漏。 所以 这里被继承自 弱引用。
验证来了。上代码,看看 ThreadLocal 被置为空后。 是否被gc了。
/**
* 首先 我创建了两个 ThreadLocal 对象 分别是 threadLocal,threadLocal_2
* 分别往 主线程 main 中 存放数字 1,2
* 然后将 threadLocal 置为空。 然后再 手动 gc 。
* 分别再gc 前 和 gc 后 查看 当前 线程中的 ThreadLocalMap。
* 由于获取 ThreadLocalMap 访问级别为default 所以我这里将用 debug的方式进行查看。
*/
public class Test1 {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal_2 = new ThreadLocal<>();
threadLocal.set(1);
threadLocal_2.set(2);
System.out.println(String.format("线程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get()));
threadLocal_2.get();
threadLocal = null;
System.gc();
threadLocal_2.get();
}
}
未gc前。debug 看到的 情况。
gc 后。 debug 看到的情况。
终上所诉。验证了 弱引用的实际效果。 和 为什么不用强引用 的原因。
这里,我们发现了。 弱引用后, ThreadLocalMap 中对应的 entry 的 referent (指向 threadLocal的引用)确实被gc了。
继续看源码。 referent 是哪里来的。 entry 继承自 WeakReference<ThreadLocal<?>> 。而 WeakReference 又继承自 抽象类 Reference<T>。
private T referent; /* Treated specially by GC */
volatile ReferenceQueue<? super T> queue;
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
volatile Reference next;
好,了解到这里。 我们不难发现。 虽然被gc了。但是 还是存在一个问题,ThreadLocalMap -》entry -》key -》 referent (threadLocal)已然为null了。但是却仍然存在ThreadLocalMap中,占用的部分的内存。 应用不可能通过某个引用再次拿到 entry 中的value了。 那不就是内存泄漏了吗?
好问题! 这里编写人员,也想到了该问题。所以,他们的处理逻辑是:再调用 set,remove 方法时,调用方法 expungeStaleEntry 将 键为null 的对象remove掉。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果entry的key为null,则清除掉该entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null) // 对该entry,重新进行hash并解决冲突
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
return i;
}
到这里,这样处理,就万事大吉了吗? 远远没有这么简单。 新问题又来了。 假使 set完后,就再也没有调用 set 和 remove 方法。那 不还是内存泄漏了吗?
所以再使用 ThreadLocal 时,要养成一个好习惯,ThreadLocal 再没有用时,就将 ThreadLocal 置为null。以免出现内存泄漏。
最后,我们来分析一个问题。ThreadLocal 是线程私有的。如果是多线程中(多线程中的线程是可复用的)使用了 ThreadLocal 会有什么问题?
答案也很简单。如果没有及时清理 ThreadLocal 除内存泄露外,还可能引发数据问题。 话不多说, 上代码。
/**
* 先后创建6个任务,前3个线程写数据,后3个线程读取数据。
*
* 发现threadLocal 里的 值被取出了。
*
* 假使有个业务场景是 往当前线程 存放 用户名(采用ThreadLocal来存储) 。
* 先进行非空判断,再进行 存储。 结果,悲剧了。 上个线程的 ThreadLocal 并没有清理。导致 不为空。
* 结果上线文中存储的用户名就乱套了。可能就张冠李戴了
*/
public class Test2 {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
for(int i=0;i<3;i++){
fixedThreadPool.execute(()->{
try {
threadLocal.set("ckr");
} catch (Exception e) {
e.printStackTrace();
}
});
}
try{
Thread.sleep(2000);
}catch(Exception ex){
}
for(int i=0;i<3;i++){
fixedThreadPool.execute(()->{
try {
System.out.println(threadLocal.get());
} catch (Exception e) {
e.printStackTrace();
}
});
}
try{
Thread.sleep(2000);
}catch(Exception ex){
}
fixedThreadPool.shutdown();
}
}
以上。关于ThreadLocal的一些问题,我们都了解了,再次,特别强调,ThreadLocal 有风险。需要谨慎使用。
关于 java 中的四种引用强,软,弱,虚。 请查看上篇博客: https://my.oschina.net/u/4141142/blog/4517998