原文地址
回顾
Entry的Key为什么设计成弱引用?
ThreadLocal用法:get(),set(),remove()
父子线程(线程池)共享数据
1. 为什么要用 ThreadLocal? 并发编程是一项非常重要的技术,它让我们的程序变得更加高效。
但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现 线程安全 问题,即 该变量最终结果可能出现异常 。
为了解决线程安全问题,JDK 出现了很多技术手段,比如:使用 synchronized 或 Lock ,给访问公共资源的代码上锁,保证了代码的原子性。
但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。
因此,JDK 还提供了另外一种 $\underline{\text{用空间换时间}}$ 的新思路:ThreadLocal。
它的核心思想是: $\underline{\text{共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。}}$
示例:每个线程获取独立的线程ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.concurrent.atomic.AtomicInteger;public class ThreadId { private static final AtomicInteger nextId = new AtomicInteger (0 ); private static final ThreadLocal<Integer> threadId = new ThreadLocal <Integer>() { @Override protected Integer initialValue () { return nextId.getAndIncrement(); } }; public static int get () { return threadId.get(); } }
2. ThreadLocal 的原理是什么? 为了搞清楚 ThreadLocal 的底层实现原理,我们不得不扒一下源码。
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 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 55 56 57 public class ThreadLocal <T> { ... 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(); } 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; } public void set (T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set(this , value); else createMap(t, value); } static class ThreadLocalMap { ... } ... }
ThreadLocal 的 get 方法、set 方法和 setInitialValue 方法,其实最终操作的都是 ThreadLocalMap 类中的数据。
其中 ThreadLocalMap 类的内部如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } ... private Entry[] table; ... }
ThreadLocalMap 里面包含一个静态的内部类 Entry,该类继承于 WeakReference 类,说明 Entry 是一个弱引用。
ThreadLocalMap 内部还包含了一个 Entry 数组,其中:
1 Entry: Key{ThreadLocal} Value{Object}
而 ThreadLocalMap 被定义成了 Thread 类的成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null ; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null ; }
下面用一张图从宏观上,认识一下 ThreadLocal 的整体结构:
从上图中看出,在每个 Thread 类中,都有一个 ThreadLocalMap 的成员变量,该变量包含了一个 Entry 数组,该数组真正保存了 ThreadLocal 类 set 的数据。
Entry 是由 threadLocal 和 value 组成,其中 threadLocal 对象是弱引用,在 GC 的时候,会被自动回收。而 value 就是 ThreadLocal 类 set 的数据。
下面用一张图总结一下引用关系:
上图中除了 Entry 的 key 对 ThreadLocal 对象是弱引用,其他的引用都是强引用。
需要特别说明的是, $\underline{\text{上图中 ThreadLocal 对象我画到了堆上,其实在实际的业务场景中不一定在堆上。}}$ 因为如果 ThreadLocal 被定义成了 static 的,ThreadLocal 的对象是类共用的,可能出现在方法区。
3. 为什么用 ThreadLocal 做 key?
存储多个值,ok
假如使用 Thread 做 key 时,你的代码中定义了 3 个 ThreadLocal 对象,那么,通过 Thread 对象,它怎么知道要获取哪个 ThreadLocal 对象呢?
如下图所示:
因此,不能使用 Thread 做 key,而应该改成用 ThreadLocal 对象做 key,这样才能通过具体 ThreadLocal 对象的 get 方法,轻松获取到你想要的 ThreadLocal 对象。
如下图所示:
4. Entry 的 key 为什么设计成弱引用? 前面说过,Entry 的 key,传入的是 ThreadLocal 对象,使用了 WeakReference 对象,即被设计成了弱引用。
那么,为什么要这样设计呢?
假如 key 对 ThreadLocal 对象的弱引用,改为强引用。
我们都知道 ThreadLocal 变量对 ThreadLocal 对象是有强引用存在的。
即使 ThreadLocal 变量生命周期完了,设置成 null 了,但由于 key 对 ThreadLocal 还是强引用。
此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。
就会存在这样的强引用链:Thread 变量 -> Thread 对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal 对象。
那么,ThreadLocal 对象和 ThreadLocalMap 都将不会被 GC 回收,于是产生了内存泄露问题。
为了解决这个问题,JDK 的开发者们把 Entry 的 key 设计成了弱引用。
弱引用的对象,在 GC 做垃圾清理的时候,就会被自动回收了。
如果 key 是弱引用,当 ThreadLocal 变量指向 null 之后,在 GC 做垃圾清理的时候,key 会被自动回收,其值也被设置成 null。
如下图所示:
下面看看弱引用的例子:
1 2 3 4 5 6 public static void main (String[] args) { WeakReference<Object> weakReference0 = new WeakReference <>(new Object ()); System.out.println(weakReference0.get()); System.gc(); System.out.println(weakReference0.get()); }
打印结果:
1 2 java.lang.Object@1ef7fe8e null
传入 WeakReference 构造方法的是直接 new 处理的对象,没有其他引用,在调用 gc 方法后,弱引用对象会被自动回收。
但如果出现下面这种情况:
1 2 3 4 5 6 7 public static void main (String[] args) { Object object = new Object (); WeakReference<Object> weakReference1 = new WeakReference <>(object); System.out.println(weakReference1.get()); System.gc(); System.out.println(weakReference1.get()); }
执行结果:
1 2 java.lang.Object@1ef7fe8e java.lang.Object@1ef7fe8e
先定义了一个强引用 object 对象,在 WeakReference 构造方法中将 object 对象的引用作为参数传入。这时,调用 gc 后,弱引用对象不会被自动回收。
我们的 Entry 对象中的 key 不就是第二种情况吗?在 Entry 构造方法中传入的是 ThreadLocal 对象的引用。
如果将 object 强引用设置为 null:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { Object object = new Object (); WeakReference<Object> weakReference1 = new WeakReference <>(object); System.out.println(weakReference1.get()); System.gc(); System.out.println(weakReference1.get()); object=null ; System.gc(); System.out.println(weakReference1.get()); }
执行结果:
1 2 3 java.lang.Object@6f496d9f java.lang.Object@6f496d9f null
第二次 gc 之后,弱引用能够被正常回收。
由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被 GC 回收。也就是说这种情况下 Entry 的 key,一直都不会为 null,除非强引用主动断开关联。
此外,你可能还会问这样一个问题:Entry 的 value 为什么不设计成弱引用?
答:Entry 的 value 假如只是被 Entry 引用,有可能没被业务系统中的其他地方引用。如果将 value 改成了弱引用,被 GC 贸然回收了(数据突然没了),可能会导致业务系统出现异常。
而相比之下,Entry 的 key,管理的地方就非常明确了。
这就是 Entry 的 key 被设计成弱引用,而 value 没被设计成弱引用的原因。
5. ThreadLocal 真的会导致内存泄露? 通过上面的 Entry 对象中的 key 设置成弱引用,并且使用 get、set 或 remove 方法清理 key 为 null 的 value 值,就能彻底解决内存泄露问题?
答案是否定的。
如下图所示:
假如 ThreadLocalMap 中存在很多 key 为 null 的 Entry,但后面的程序,一直都没有调用过有效的 ThreadLocal 的 get、set 或 remove 方法。
那么,Entry 的 value 值一直都没被清空。
所以会存在这样一条强引用链:Thread 变量 -> Thread 对象 -> ThreadLocalMap -> Entry -> value -> Object。
其结果就是:Entry 和 ThreadLocalMap 将会长期存在下去,会导致内存泄露。
6. 如何解决内存泄露问题? 前面说过的 ThreadLocal 还是会导致内存泄露的问题,我们有没有解决办法呢?
答:有办法,调用 ThreadLocal 对象的 remove 方法。
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 55 56 57 58 59 public void remove () { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove(this ); } 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 ; } } } private T referent; public void clear () { this .referent = null ; } private int expungeStaleEntry (int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null ; tab[staleSlot] = null ; size--; Entry e; int i; for (i = nextIndex(staleSlot, len);(e = tab[i]) != null ; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; tab[i] = null ; size--; } else { int h = k.threadLocalHashCode & (len - 1 ); if (h != i) { tab[i] = null ; while (tab[h] != null ) h = nextIndex(h, len); tab[h] = e; } } } return i; }
不是在一开始就调用 remove 方法,而是在使用完 ThreadLocal 对象之后。
示例:
先创建一个 CurrentUser 类,其中包含了 ThreadLocal 的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class CurrentUser { private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal (); public static void set (UserInfo userInfo) { THREA_LOCAL.set(userInfo); } public static UserInfo get () { THREA_LOCAL.get(); } public static void remove () { THREA_LOCAL.remove(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void doSamething (UserDto userDto) { UserInfo userInfo = convert(userDto); try { CurrentUser.set(userInfo); ... UserInfo userInfo = CurrentUser.get(); ... } finally { CurrentUser.remove(); } }
需要我们特别注意的地方是:一定要在 finally 代码块中,调用 remove 方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。
remove 方法中会把 Entry 中的 key 和 value 都设置成 null,这样就能被 GC 及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
7. ThreadLocal 是如何定位数据的? 前面说过 ThreadLocalMap 对象底层是用 Entry 数组保存数据的。
那么问题来了,ThreadLocal 是如何定位 Entry 数组数据的?
在 ThreadLocal 的 get、set、remove 方法中都有这样一行代码:
1 int i = key.threadLocalHashCode & (len-1 );
通过 key 的 hashCode 值,与数组的长度减 1。其中 key 就是 ThreadLocal 对象,与数组的长度减 1,$\underline{\text{相当于除以数组的长度减 1,然后取模}}\text{ -> len=16,和15相与即为取低四位}$ 。
使用与运算效率跟高一些。
为什么与运算效率更高?
答:因为 ThreadLocal 的初始大小是 16,每次都是按 2 倍扩容,数组的大小其实一直都是 2 的 n 次方。这种数据有个规律就是高位是 0,低位都是 1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是 0。只需考虑低位的与运算,所以效率更高。
如果使用 hash 算法定位具体位置的话,就可能会出现 hash 冲突的情况,即两个不同的 hashCode 取模后的值相同。
ThreadLocal 是如何解决 hash 冲突的呢?
我们看看 getEntry 是怎么做的:
1 2 3 4 5 6 7 8 9 10 11 12 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); }
再看看 getEntryAfterMiss 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 ; }
关键看看 nextIndex 方法:
1 2 3 private static int nextIndex (int i, int len) { return ((i + 1 < len) ? i + 1 : 0 ); }
当通过 hash 算法计算出的下标小于数组大小,则将下标值加 1。否则,即下标大于等于数组大小,下标变成 0 了。下标变成 0 之后,则循环一次,下标又变成 1。。。
寻找的大致过程如下图所示:
如果找到最后一个,还是没有找到,则再从头开始找。
不知道你有没有发现,它构成了一个:环形。
1 2 3 4 5 6 7 8 ThreadLocal 从数组中找数据的过程大致是这样的: 1. 通过 key 的 hashCode 取余计算出一个下标。 2. 通过下标,在数组中定位具体 Entry,如果 key 正好是我们所需要的 key,说明找到了,则直接返回数据。 3. 如果第 2 步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。 4. 如果第 3 步中找 key 的正好是我们所需要的 key,说明找到了,则直接返回数据。 5. 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为 0 的位置,继续从前往后找数据。 6. 直到找到第一个 Entry 为空为止。
8. ThreadLocal 是如何扩容的? 从上面得知,ThreadLocal 的初始大小是 16。那么问题来了,ThreadLocal 是如何扩容的?
在 set 方法中会调用 rehash 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void set (ThreadLocal<?> key, Object value) { 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(); }
注意一下,其中有个判断条件是:sz(之前的 size+1) 如果大于或等于 threshold 的话,则调用 rehash 方法。
threshold 默认是 0,在创建 ThreadLocalMap 时,调用它的构造方法:
1 2 3 4 5 6 7 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); }
调用 setThreshold 方法给 threshold 设置一个值,而这个值 INITIAL_CAPACITY 是默认的大小 16。
1 2 3 private void setThreshold (int len) { threshold = len * 2 / 3 ; }
也就是第一次设置的 threshold = 16 * 2 / 3, 取整后的值是:10。
换句话说当 sz 大于等于 10 时,就可以考虑扩容了。
rehash 代码如下:
1 2 3 4 5 6 7 private void rehash () { expungeStaleEntries(); if (size >= threshold - threshold / 4 ) resize(); }
在真正扩容之前,先尝试回收一次 key 为 null 的值,腾出一些空间。
如果回收之后的 size 大于等于 threshold 的 3/4 时,才需要真正的扩容。
计算公式如下:
1 16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
也就是说添加数据后,新的 size 大于等于老 size 的 1/2 时,才需要扩容。
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 private void resize () { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2 ; Entry[] newTab = new Entry [newLen]; int count = 0 ; for (int j = 0 ; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null ) { ThreadLocal<?> k = e.get(); if (k == null ) { e.value = null ; } else { int h = k.threadLocalHashCode & (newLen - 1 ); while (newTab[h] != null ) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
resize 中每次都是按 2 倍的大小扩容。
扩容的过程如下图所示:
1 2 3 4 5 6 7 扩容的关键步骤如下: 1. 老 size + 1 = 新 size 2. 如果新 size 大于等于老 size 的 2/3 时,需要考虑扩容。 3. 扩容前先尝试回收一次 key 为 null 的值,腾出一些空间。 4. 如果回收之后发现 size 还是大于等于老 size 的 1/2 时,才需要真正的扩容。 5. 每次都是按 2 倍的大小扩容。
9. 父子线程如何共享数据? 前面介绍的 ThreadLocal 都是在一个线程中保存和获取数据的。
但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往 ThreadLocal 设置了值,在子线程中能够获取到。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 public class ThreadLocalTest { public static void main (String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal <>(); threadLocal.set(6 ); System.out.println("父线程获取数据:" + threadLocal.get()); new Thread (() -> { System.out.println("子线程获取数据:" + threadLocal.get()); }).start(); } }
执行结果:
你会发现,在这种情况下使用 ThreadLocal 是行不通的。main 方法是在主线程中执行的,相当于父线程。在 main 方法中开启了另外一个线程,相当于子线程。
显然通过 ThreadLocal,无法在父子线程中共享数据。
那么,该怎么办呢?
答:使用 InheritableThreadLocal ,它是 JDK 自带的类,继承了 ThreadLocal 类。
修改代码之后:
1 2 3 4 5 6 7 8 9 10 11 12 public class ThreadLocalTest { public static void main (String[] args) { InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal <>(); threadLocal.set(6 ); System.out.println("父线程获取数据:" + threadLocal.get()); new Thread (() -> { System.out.println("子线程获取数据:" + threadLocal.get()); }).start(); } }
执行结果:
果然,在换成 InheritableThreadLocal 之后,在子线程中能够正常获取父线程中设置的值。
其实,在 Thread 类中除了成员变量 threadLocals 之外,还有另一个成员变量:inheritableThreadLocals。
Thread 类的部分代码如下:
1 2 ThreadLocal.ThreadLocalMap threadLocals = null ; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null ;
最关键的一点是,在它的 init 方法中会将父线程中往 ThreadLocal 设置的值,拷贝一份到子线程中。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 public Thread () { init(null , null , "Thread-" + nextThreadNum(), 0 ); } private void init (ThreadGroup g, Runnable target, String name,long stackSize) { init(g, target, name, stackSize, null , true ); } private void init (ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { .... Thread parent = currentThread(); .... this .group = g; this .daemon = parent.isDaemon(); this .priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this .contextClassLoader = parent.getContextClassLoader(); else this .contextClassLoader = parent.contextClassLoader; this .inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this .target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null ) this .inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this .stackSize = stackSize; tid = nextThreadID(); } static ThreadLocalMap createInheritedMap (ThreadLocalMap parentMap) { return new ThreadLocalMap (parentMap); } private ThreadLocalMap (ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry [len]; for (int j = 0 ; j < len; j++) { Entry e = parentTable[j]; if (e != null ) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null ) { Object value = key.childValue(e.value); Entry c = new Entry (key, value); int h = key.threadLocalHashCode & (len - 1 ); while (table[h] != null ) h = nextIndex(h, len); table[h] = c; size++; } } } }
10. 线程池中如何共享数据? 在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池。
那么,在线程池中如何共享 ThreadLocal 对象生成的数据呢?
因为涉及到不同的线程,如果直接使用 ThreadLocal ,显然是不合适的。
我们应该使用 InheritableThreadLocal ,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static void fun1 () { InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal <>(); threadLocal.set(6 ); System.out.println("父线程获取数据:" + threadLocal.get()); ExecutorService executorService = Executors.newSingleThreadExecutor(); threadLocal.set(6 ); executorService.submit(() -> { System.out.println("第一次从线程池中获取数据:" + threadLocal.get()); }); threadLocal.set(7 ); executorService.submit(() -> { System.out.println("第二次从线程池中获取数据:" + threadLocal.get()); }); }
执行结果:
1 2 3 父线程获取数据:6 第一次从线程池中获取数据:6 第二次从线程池中获取数据:6
由于这个例子中使用了单例线程池,固定线程数是 1。
第一次 submit 任务的时候,该线程池会自动创建一个线程。因为使用了 InheritableThreadLocal ,所以创建线程时,会调用它的 init 方法,将父线程中的 inheritableThreadLocals 数据复制到子线程中。所以我们看到,在主线程中将数据设置成 6,第一次从线程池中获取了正确的数据 6。
之后,在主线程中又将数据改成 7,但在第二次从线程池中获取数据却依然是 6。
因为第二次 submit 任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的 init 方法,所以第二次其实没有获取到最新的数据 7,还是获取的老数据 6。
那么,这该怎么办呢?
答:使用 TransmittableThreadLocal,它并非 JDK 自带的类,而是阿里巴巴开源 jar 包中的类。
可以通过如下 pom 文件引入该 jar 包:
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > transmittable-thread-local</artifactId > <version > 2.11.0</version > <scope > compile</scope > </dependency >
代码调整如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static void fun2 () throws Exception { TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal <>(); threadLocal.set(6 ); System.out.println("父线程获取数据:" + threadLocal.get()); ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1 )); threadLocal.set(6 ); ttlExecutorService.submit(() -> { System.out.println("第一次从线程池中获取数据:" + threadLocal.get()); }); threadLocal.set(7 ); ttlExecutorService.submit(() -> { System.out.println("第二次从线程池中获取数据:" + threadLocal.get()); }); }
执行结果:
1 2 3 父线程获取数据:6 第一次从线程池中获取数据:6 第二次从线程池中获取数据:7
我们看到,使用了 TransmittableThreadLocal 之后,第二次从线程中也能正确获取最新的数据 7 了。
如果你仔细观察这个例子,你可能会发现,代码中除了使用 TransmittableThreadLocal 类之外,还使用了 TtlExecutors.getTtlExecutorService 方法,去创建 ExecutorService 对象。
这是非常重要的地方,如果没有这一步,TransmittableThreadLocal 在线程池中共享数据将不会起作用。
创建 ExecutorService 对象,底层的 submit 方法会 TtlRunnable 或 TtlCallable 对象。
以 TtlRunnable 类为例,它实现了 Runnable 接口,同时还实现了它的 run 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void run () { Map<TransmittableThreadLocal<?>, Object> copied = (Map)this .copiedRef.get(); if (copied != null && (!this .releaseTtlValueReferenceAfterRun || this .copiedRef.compareAndSet(copied, (Object)null ))) { Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied); try { this .runnable.run(); } finally { TransmittableThreadLocal.restoreBackup(backup); } } else { throw new IllegalStateException ("TTL value reference is released after run!" ); } }
1 2 3 4 5 这段代码的主要逻辑如下: 1. 把当时的 ThreadLocal 做个备份,然后将父类的 ThreadLocal 拷贝过来。 2. 执行真正的 run 方法,可以获取到父类最新的 ThreadLocal 数据。 3. 从备份的数据中,恢复当时的 ThreadLocal 数据。
11. ThreadLocal 有哪些用途? 老实说,使用 ThreadLocal 的场景挺多的。
下面列举几个常见的场景:
在 spring 事务中,保证一个线程下,一个事务的多个操作拿到的是一个 Connection。
在 hiberate 中管理 session。
在 JDK8 之前,为了解决 SimpleDateFormat 的线程安全问题。
获取当前登录用户上下文。
临时保存权限数据。
使用 MDC 保存日志信息。
等等,还有很多业务场景,这里就不一一列举了。
接下来留几个问题给大家思考一下:
ThreadLocal变量为什么建议要定义成static的? ok
Entry数组为什么要通过hash算法计算下标,即直线寻址法,而不直接使用下标值? hash散列
强引用和弱引用有什么区别? gc
Entry数组大小,为什么是2的N次方? 低位,计算机操作2进制
使用InheritableThreadLocal时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗? 源码,获取后即为新建的Map