ThreadLocal原理解析!
发表于更新于
编程语言JavaThreadLocal原理解析!
月伴飞鱼
ThreadLocal
提供了线程本地变量的实例,它与普通变量的区别在于。
- 每个使用该变量的线程都会初始化一个完全独立的实例副本。
使用场景
ThreadLocal
用作保存每个线程独享的对象,为每个线程都创建一个副本。
- 这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
ThreadLocal
用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景,避免了传参。
用户登录态上下文:
- 通过
TheadLocal
封装用户公共的上下文信息,可以将身份鉴定、权限等一系列公用内容统一处理,服务层直接应用。
关键属性
1 2 3 4 5 6 7 8 9
| private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
private static AtomicInteger nextHashCode = new AtomicInteger();
|
ThreadLocalMap
ThreadLocalMap
本身就是一个简单的 Map
结构。
Key
是 ThreadLocal
,Value
是 ThreadLocal
保存的值,底层是数组的数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int threshold; }
|
ThreadLocal 是如何做到线程之间数据隔离的
主要因为是 ThreadLocalMap
是线程的属性。
ThreadLocals.ThreadLocalMap
和 InheritableThreadLocals.ThreadLocalMap
分别是线程的属性。
- 所以每个线程的
ThreadLocals
都是隔离独享的。
父线程在创建子线程的情况下,会拷贝 inheritableThreadLocals
的值,但不会拷贝 threadLocals
的值。
set 方法
1 2 3 4 5 6 7 8 9 10 11
| public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
|
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
| 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(); }
|
通过递增的 AtomicInteger
作为 ThreadLocal
的 hashCode
的。
计算数组索引位置的公式是:
hashCode
取模数组大小,由于 hashCode
不断自增。
- 所以不同的
hashCode
大概率上会计算到同一个数组的索引位置(在实际项目中,ThreadLocal
都很少,基本上不会冲突)
通过 hashCode
计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找。
- 直到找到索引位置为空的地方,把当前
ThreadLocal
作为 key
放进去。
get 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; }
|
扩容
ThreadLocalMap
中的 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
| 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; }
|
扩容后数组大小是原来数组的两倍。
扩容时是没有线程安全问题的,因为 ThreadLocalMap
是线程的一个属性。
一个线程同一时刻只能对 ThreadLocalMap
进行操作,因为同一个线程执行业务逻辑必然是串行的。
- 那么操作
ThreadLocalMap
必然也是串行的。
内存泄漏
ThreadLocalMap
的每个Entry
都是一个对key
的弱引用,同时每个Entry都包含了一个对value的强引用。
- 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。
但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收。
- 因为有以下的调用链:
Thread ---> ThreadLocalMap ---> Entry(key为null) ---> value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。
JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry。
- 并把对应的value设置为null,这样value对象就可以被回收。
如何避免内存泄露
调用remove方法,就会删除对应的Entry对象,可以避兔内存泄漏。
- 所以使用完ThreadLocal之后,应该调用remove方法。
1 2 3 4 5 6 7
| ThreadLocal<String> localName = new ThreadLocal(); try { localName.set("月伴飞鱼"); } finally { localName.remove(); }
|
空指针问题
ThreadLocal在进行get之前,必须先set,否则会报空指针异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ThreadLocalNPE { ThreadLocal<Long> longThreadLocal = new ThreadLocal<>(); public void set() { longThreadLocal.set(Thread.currentThread().getId()); } public Long get() { Long res = longThreadLocal.get(); return res; } public static void main(String[] args) { ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); System.out.println(threadLocalNPE.get()); } }
|
如果在每个线程中ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static对象。
那么多个线程的 ThreadLocal.get()
取得的还是这个共享对象本身,还是有并发访问问题。
InheritableThreadLocal
InheritableThreadLocal
解决父子线程变量传递的问题。
- 如果我在后面改了父线程,子线程不会更新它的本地变量
Map
。
这个ThreadLocalMap
的局部变量,实际作用是在子线程创建的时候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class InheritableThreadLocalSolution {
private static final ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>(); private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) { userNameThreadLocal.set("Parent Thread"); inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");
ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(() -> { System.out.println("From child thread (ThreadLocal): " + userNameThreadLocal.get()); System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get()); }); executorService.shutdown(); } }
|
InheritableThreadLocal的内存泄漏
当移除父线程的threadlocal
,子线程的threadlocal
并不会消失。
- 并且通常来讲子线程是放线程池管理的,不会随着父线程的消失而消失。
所以子线程的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
| public class InheritableThreadLocalSolution {
private static final InheritableThreadLocal<String> inheritableUserNameThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException { inheritableUserNameThreadLocal.set("Parent Thread (Inheritable)");
ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get()); });
TimeUnit.SECONDS.sleep(5); inheritableUserNameThreadLocal.remove(); executorService.submit(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("second From child thread (InheritableThreadLocal): " + inheritableUserNameThreadLocal.get()); }); TimeUnit.SECONDS.sleep(10); executorService.shutdown(); } }
|
TransmittableThreadLocal
项目地址:https://gitee.com/mirrors/transmittable-thread-local
对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的。
- 这时父子线程关系的
ThreadLocal
值传递已经没有意义
- 需要把 任务提交给线程池时的
ThreadLocal
值传递到 任务执行时。
TransmittableThreadLocal
解决线程池变量丢失问题。
- 线程池会复用之前的线程,导致父线程的本地变量更新之后,之前创建的子线程拿不到这个值。
get/set
方法中完成了TransmittableThreadLocal
的注册
- 然后在执行run方法的时候通过
TtlRunnable
进行了方法包装
在调用之前进行快照形成,并应用快照到当前线程中