ThreadLocal是什么? ThreadLocal是线程之间进行协作和共享的重要工具,可以保证并发中的线程安全。
当我们定义好了ThreadLocal变量后,每个线程就会持有这个变量的副本,各个线程就可以单独操作这些变量而不会互相影响,当然我们也可以使用锁的方式来代替,用ThreadLocal这种方案不过是用了空间来换时间了。
一句话,ThreadLocal可以避免并发场景下的线程安全问题,因为你访问ThreadLocal变量实际操作的是自己线程中保存的副本。
ThreadLocal的使用 举个例子,在main线程中开启五个线程,每个线程都去修改 express对象 中的ThreadLocal变量的值,我们来看看是个什么情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Express { //定义 ThreadLocal 变量,并将它的值初始化成100 ThreadLocal<Integer> tl = new ThreadLocal<Integer>() { //重写 initialVlaue() 方法,将其赋值100 @Override protected Integer initialValue() { return 100; } }; public Express(){} public void changeThreadLocalVariable(int num) { //set() 方法,用来设置 ThreadLocal 的值 tl.set(num); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test { //静态变量 express private static final Express express = new Express(); public static class A extends Thread { @Override public void run() { //以每个 Thread 的 id 作为值,重新设置当前线程中的 ThreadLocal 变量副本的值 int id = (int) Thread.currentThread().getId(); //副本设值 express.changeThreadLocalVariable(id); System.out.println(Thread.currentThread().getName() + "修改后的值为:" + express.tl.get()); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { //以此启动五个线程 new A().start(); } System.out.println("num实际的值为:" + express.tl.get()); } }
运行后结果如下:我们发现实际的num值依然是100,而其他五个子线程中的num值则已经被修改了,这便证实了 ThreadLocal 变量,会让每个线程拥有其一个副本,在子线程中都是访问和操作这个副本。
1 2 3 4 5 6 Thread-0修改后的值为:20 Thread-4修改后的值为:24 Thread-1修改后的值为:21 Thread-2修改后的值为:22 Thread-3修改后的值为:23 num实际的值为:100
ThreadLocal的原理解析 在 Thread 类中有一个成员变量是 threadLocals
1 ThreadLocal.ThreadLocalMap threadLocals = null;
这个变量的作用就是用来保存其它线程中 ThreadLocal 变量的副本。因此每个线程中都会有一个 threadLocals
,不过最开始都是 null,等待着你给它设置。下面给大家画了一张图,便于整体上对 ThreadLocal
一个总体的认识。
从上面这张图我们可以大概知道:
1.每个线程中都会有一个 ThreadLocalMap
变量。
2.ThreadLocalMap
中内部是一个个 Entry
,其中的 key 是 ThreadLocal
本身,而 value 是创建 ThreadLocal
时的值。
ThreadLocalMap 源码 以下仅展示部分相关源码,已省略掉无关代码,便于大家理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static class ThreadLocalMap { //弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { private static final int INITIAL_CAPACITY = 16; Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //Entry数组,用于多个 ThreadLocal<T> 的储存 private Entry[] table; //创建ThreadLocalMap,ThreadLocal作为key,以ThreadLocal中的泛型为值 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //创建 Entry table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } }
set() 方法源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //拿到当前线程的 ThreadLocal.ThreadLocalMap 变量 threadLocals ThreadLocalMap map = getMap(t); if (map != null) //如果不为 threadLocals 不为空就调用 ThreadLocalMap 中的 set 方法 map.set(this, value); else //空的话就创建一个 ThreadLocalMap 变量,并把 value 保存进去 createMap(t, value); ------------------------------------------------------------------------- //ThreadLocalMap 中的 set() 方法 //这里是真正给entry设置的地方 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; //经过复杂位运算获得entry数组中的对应的下表 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; } //对 key 为 null 的entry清除,但是清除不是非常及时,会造成内存泄漏,因此当我们用完ThreadLocal时一定要记得 remove() if (k == null) { replaceStaleEntry(key, value, i); return; } } //创建entry并保存值 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } }
get() 方法源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public T get() { //获取当前线程 Thread t = Thread.currentThread(); //拿到当前线程的 ThreadLocal.ThreadLocalMap 变量 threadLocals ThreadLocalMap map = getMap(t); if (map != null) { //如果不为 threadLocals 不为空就调用 getEntry() 方法 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //如果 e 不为 null,就返回entry的value T result = (T)e.value; return result; } } //如果map是空,就调用setInitialValue()方法,实际上最终返回的是 null return setInitialValue(); } ------------------------------------------------------------------------- //getEntry() 方法 //真正得到 value 的方法 private Entry getEntry(ThreadLocal<?> key) { //经过复杂位运算获得entry数组中的对应的下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) //entry 不为 null 且有相应的 key 就返回对应数组的元素值 return e; else //不满足上述条件的话就会调用 getEntryAfterMiss() return getEntryAfterMiss(key, i, e); }
getEntryAfterMiss()这个方法会判断 entry
是否为空,如果为空就会直接返回 null
,否则将进一步判断 key 是否为空,若不为空就返回此 entry
,如果为空那么就会做一波清理,清理掉过时的 entry
。
remove() 方法源码 1 2 3 4 5 6 //这个方法就是用来清除 threadLocals 的,防止内存泄漏 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
ThreadLocal 的 Entry 中的 Key 为什么要用弱引用? 1 2 3 4 5 6 7 8 9 10 //WeakReference --------------> 弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
在说这个问题之前我们先来看看 JDK
为我们提供了哪些引用:
强引用 : 像 Objective o = new Object()
这种直接用 =
引用的就属于强引用,在每次 GC
时会进行 根可达分析 ,如果能够与 GCRoots 相关联,那么就永远不会被垃圾回收。
软引用 :一些有用但是并非必需。用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收,如果这次回收后还是没有足够的空间,才会抛出内存溢出。
弱引用 :一些有用(程度比软引用更低)但是并非必需。用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。
虚引用 :最弱,它随时可能被回收掉。垃圾回收的时候收到一个通知,主要用来监控垃圾回收器是否正常工作。
照这么说的话弱引用能够在 GC
时被清除掉,那用在 Entry中的Key
中有什么用呢,用强引用难道就不行了吗?
诶,答案是还真不行。我们来看看 Entry
在堆里的结构:
如果 Entry
中的 key
是强引用,那么当 ThreadLocal
变量 tl
手动设置为 null 时,由于 Entry
中还有对 ThreadLocal
对象的强引用,因此它无法被回收。而我们栈中 tl
变量已经是 null
,无法通过 key
来访问 Entry
中的变量了,但这个 Entry
由于持有一个强引用又不会被垃圾回收,因此这就造成了 内存泄漏 。
那我们来看看用 弱引用 是什么情况,当我们使用完 tl
变量后将其设置为 null
后,因为是 弱引用
,因此 ThreadLocal
会被垃圾回收。当被回收之后, Entry
中的 key
就是 null
了,而之前我们讲到过在 get()
,set()
,remove()
方法中都会对 key
为 null
的 entry
进行清除处理,因此不会造成内存泄漏,这就是 JDK
用 虚引用 的精妙之处。
因此建议大家在用完 ThreadLocal
变量之后一定记得要进行 tl.remove()
操作,用来避免内存泄漏这一问题。