1、ThreadLocal 线程私有的。java
为何说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 。再往里存放数据。jvm
接下来咱们再来看ThreadLocalMap 中 key ,value 究竟又存放的是什么。显然 ThreadLocalMap 中 key ->ThreadLocal ;value ->咱们set 进去的 value。this
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是线程私有的。spa
接下来,咱们用代码来证实.net
/** * 首先 我先初始化一个 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 的成员变量 threadLocalsdebug
/*java源码中包含一个 ThreadLocalMap 的成员变量 threadLocals */ /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
从上图咱们能够看出ThreadLocalMap 是 ThreadLocal 中的 静态内部类。code
1.2 接着咱们来看下 ThreadLocal 中的源码。orm
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 中的四种引用强,软,弱,虚。 请查看上篇博客: http://www.javashuo.com/article/p-czdxdtqo-nx.html