ThreadLocal
概念:
- ThreadLocal提供了线程本地变量,也就是如果创建一个ThreadLocal变量,那么访问的这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存的里面的变量,从而避免了线程安全问题。
- 每个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
- 扩容:使用开放寻址方法;
使用示例:
package com.example.demo; /** * @author SHshuo * @data 2021/11/6--14:17 * ThreadLocal提供了线程本地变量,也就是如果创建一个ThreadLocal变量, * 那么访问的这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时, * 实际操作的是自己本地内存的里面的变量,从而避免了线程安全问题。 * * 不具有继承性 */ public class ThreadLocalTest { // 创建ThreadLocal变量 static ThreadLocal<String> localVariable = new ThreadLocal<>(); public static void main(String[] args) { // 创建两个线程、使用ThreadLocal new Thread(() -> { localVariable.set("Thread Of A local variable"); System.out.println("Thread Of A : " + localVariable.get()); // 移除本地内存保存的副本 localVariable.remove(); // 获取的是本地内存保存的副本、而不是主内存 System.out.println("Thread Of A remove : " + localVariable.get()); }, "A").start(); new Thread(() -> { localVariable.set("Thread Of B local variable"); System.out.println("Thread Of B : " + localVariable.get()); // 移除本地内存保存的副本 localVariable.remove(); System.out.println("Thread Of B remove : " + localVariable.get()); }, "B").start(); } }
实现原理,简单来说:
- 每个线程的本地变量不是存放在ThreadLocal的实例里面,而是存放在调用线程的threadLocals变量(也就是每一个线程的本地内存的变量)里面。
- ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳。
- threadLocals是一个hashmap的结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值。
解决线程之间的值传递
ThreadLocal不具有继承性
因为子线程获取的自己本地的工作空间,通过给主线程设置值不会传递到子线程的工作空间。
package com.example.demo; /** * @author SHshuo * @data 2021/11/6--14:33 */ public class ThreadLocalTest { public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("With inheritance"); new Thread(() -> System.out.println("子线程: " + threadLocal.get())).start(); System.out.println("main: " + threadLocal.get()); } }
父子线程进行值传递
InheritableThreadLocal具有继承性;
原理:通过将本地InheritableThreadLocals变量复制一份到子线程InheritableThreadLocals变量里实现继承。、
场景:
- 子线程需要存放ThreadLocal变量中的用户登录信息。
- 中间件需要把统一的id追踪的整个调用链路记录下来。等等
package com.example.demo; /** * @author SHshuo * @data 2021/11/6--14:33 * 通过将本地变量复制一份到子线程变量里实现继承。获取仍然是本地内存的变量 * inheritance继承性的意思、使用InheritableThreadLocal实现 */ public class InheritableThreadLocalTest { public static void main(String[] args) { ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); inheritableThreadLocal.set("With inheritance"); new Thread(() -> System.out.println("子线程: " + inheritableThreadLocal.get())).start(); System.out.println("main: " + inheritableThreadLocal.get()); } }
线程池中的线程进行值传递
使用:TransmittableThreadLocal是阿里开源的线程池异步上线文传递的一个组件,并不是JDK自带的。
ThreadLocal的常见方法的实现
set()public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //找到对应map里面entry的位置 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }get()
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
总结:
无论set()、get()方法都会首先获取当前的Thread对象,根据thread获取当前线程的成员变量ThreadLocalMap。
从而进行判断如果map为空,就会创建key:ThreadLocal;value:设置的值。不为空,就找到对应entry(key:ThreadLocal; value:设置的值)。
由ThreadLocal引申出四种引用类型
前提介绍:
- 强引用:是指创建一个对象并把这个对象赋值给一个引用变量(new出的对象、等于号赋值)、内存不足也不会回收;
- 软引用:用于内存敏感高速缓存。内存足够不回收、内存不足就会回收;
- 弱引用:一旦发现弱引用对象就会回收、无论内存是否足够;
- 虚引用:相当于没有引用、主要用来跟踪对象被垃圾回收的活动
内存泄漏:
- 当B使用了A的对象后,即使A被置为null,但是GC仍然不会回收。因为B还持有对A的引用,而且还是强引用。
- 所以既不能回收,也不能利用使用,这就会造成内存泄漏。
A a = new A(); B b = new B(a);
针对本例题解决方法:
- 将b = null,显示的设置消除引用。就会回收这个对象。
- B使用弱引用(weakReference b = new weakReference (a))
ThreadLocal中的key引发的内存泄漏
如果ThreadLocalMap的Entry中强引用了ThreadLocal实例,即使ThreadLocal instance = null仍然无法通过GC回收。因为如果没有显示的将引用赋值为null(将entry设置为null),ThreadLocalMap仍会持有ThreadLocal的强引用、是不会被回收的(还会有引用链通过可达性分析,ThreadLocal实例仍然是可达的)。这就会造成内存泄漏。
解决:
通过ThreadLocalMap中的Entry继承WeakReference弱引用,super(k):将每一个entry的key都是弱引用。
一旦发现就会回收无论内存是否够用,也就避免了内存泄漏的问题;
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
ThreadLocal中的value引发的内存泄漏
从上面那段代码表示了强引用的发生(被赋值);
value = v;所以当线程一直不销毁,通过线程池复用的时候,value对象通过可达性分析一直是可达的,所以不会被回收。
解决:
- 通过使用remove(),扫描key为null的Entry,将里面的value显示的置为null,就可以正常被回收了。
- 通过remove防止内存泄漏;
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; } } }
总结:
内存泄漏的解决办法,实质:避免强引用;
- 使用弱引用;(解决ThreadLocalMap中的key内存泄漏)
- 通过显式的置为null消除强引用(解决ThreadLocalMap中的value内存泄漏)
思维脑图
参考:
hshuo的面试之路 文章被收录于专栏
作者目标是找到一份Java后端方向的工作 此专栏用来记录从Bilibili、书本、其他优质博客上面学习的内容 用于巩固、总结内容 主要包含Docker、Dubbo、Java基础、JUC、Maven、MySQL、Redis、SpringBoot、SpringCloud、数据结构、杂文、算法、计算机网络、操作系统、设计模式等相关内容