网站公司logo设计,163网易企业邮箱入口,微信分销网站建设平台,网站开发费用成本表ThreadLocal无论是在项目开发还是面试中都会经常碰到#xff0c;它的重要性可见一斑#xff0c;本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。
一、什么是ThreadLocal ThreadLocal是java.lang下面的一个类#xff0c;在JDK 1.2版…ThreadLocal无论是在项目开发还是面试中都会经常碰到它的重要性可见一斑本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。
一、什么是ThreadLocal ThreadLocal是java.lang下面的一个类在JDK 1.2版本加入作者是Josh Bloch集合大神和Doug Lea并发大神。 它提供了一种线程局部变量的方式线程局部变量是指每个线程都拥有自己独立的变量副本互不干扰通过ThreadLocal可以方便地在多线程环境下共享数据同时不需要考虑线程安全性这也是解决并发问题的途径之一。 例如在web开发中可以使用ThreadLocal来保存用户的登录信息以便每个线程都能够独立地获取和修改自己的登录信息避免了线程之间的干扰。 二、ThreadLocal的使用
ThreadLocal有四个方法分别为 protected T initialValue()返回此线程局部变量的初始值。 pubulic T get() 返回当前线程局部变量的当前线程副本的值。如果这是线程第一次调用该方法则创建并初始化此副本。 public void set(T value)将此线程局部变量的当前线程的副本设置为指定的值。 public void remove()移除此线程局部变量的当前线程的值。 下面使用ThreadLocal来模拟用户登录信息的场景
ThreadLocal工具类
public class CurrentUserHolder {public static ThreadLocalUser threadLocalnew ThreadLocal();
public static void setUser(User user){threadLocal.set(user);}
public static User getUser(){if (Objects.nonNull(threadLocal.get())) {return threadLocal.get();}throw new RuntimeException(当前用户信息为空);}
public static void clearUser(){threadLocal.remove();}
}
User实体类
Data
public class User {private String name;private Integer age;
}
测试
public class Test {public static void main(String[] args) {//用户登录User user new User();user.setName(小黑子);user.setAge(18);//将用户信息保存在ThreadLocal中CurrentUserHolder.setUser(user);//在其它方法中可以通过ThreadLocal获取用户信息User localUser CurrentUserHolder.getUser();System.out.println(localUser);//输出User(name小黑子, age18)//用户操作完成后可以remove掉CurrentUserHolder.clearUser();}
}
ps由于ThreadLocal是基于线程的所以在不同的线程中通过ThreadLocal获取的用户信息是独立的这在多线程环境下非常有用可以避免线程之间的数据混乱和冲突。
三、ThreadLocal的实现原理
直接上图下图中基本描述出了Thread、ThreadLocalMap和ThreadLocal三者之间的关系。 解释一下 ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类ThreadLocalMap也是k-v结构key就是当前ThreadLocal对象value就是我们要保存的值。 Thread类中维护了两个ThreadLocalMap成员变量threadLocals和inheritableThreadLocals它们的默认值是null类型为ThreadLocal.ThreadLocalMap也就是ThreadLocal类的一个静态内部类ThreadLocalMap感兴趣的可以去看一下源码。 四、核心源码
4.1 ThreadLocalMap内部类
在静态内部类ThreadLocalMap中维护了一个数据结构类型为Entry的数组源码如下
static class Entry extends WeakReferenceThreadLocal? {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}
}
从源码中我们可以看到Entry继承了一个ThreadLocal类型的弱引用并将其作为keyvalue为Object类型也就是我们需要保存的值。
我们再来看一下它的成员变量
//数组的默认初始化容量
private static final int INITIAL_CAPACITY 16;
//Entry数组大小必须为2的幂
private Entry[] table;
//数组内部元素个数
private int size 0;
//数组扩容阈值默认为0创建ThreadLocalMap对象后会被重新设置
private int threshold;
是不是有点熟悉这几个变量和HashMap中的变量很类似功能也类似。
最后看一下它的构造方法
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
注释翻译过来大概就是该构造方法是懒加载的只有我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化数组。
4.2 set()方法
接下来我们就介绍一下ThreadLocal常用的一些方法吧首先看一下set()方法
public void set(T value) {//获取当前线程Thread t Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map getMap(t);if (map ! null)// 如果map存在则将当前ThreadLocal对象作为keyvalue作为value放入map中map.set(this, value);else// 如果map不存在则创建一个新的ThreadLocalMap对象并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和valuecreateMap(t, value);
}
解释 获取当前线程拿到当前Thread的ThreadLocalMap对象。 如果map存在则将当前ThreadLocal对象作为keyvalue作为value放入map中。 如果map不存在则创建一个新的ThreadLocalMap对象并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value。 4.3 get()方法
源码如下
public T get() {//获取当前线程Thread t Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map getMap(t);if (map ! null) {//map存在通过this当前ThreadLocal获取EntryThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)//Entry不为空返回该Entry的value值T result (T)e.value;return result;}}//map不存在调用setInitialValue()方法设置初始值return setInitialValue();
}
解释 通过当前线程获取ThreadLocalMap 如果map存在则通过当前ThreadLocal获取对应的Entry若Entry不为空返回该Entry的value值。 如果map不存在则调用setInitialValue()方法设置初始值。 setInitialValue() 根据initalValue()方法获取value值默认值为null可以重写该方法。 通过当前线程获取ThreadLocalMap对象。 map存在设置当前值为上述value不存在则创建新的ThreadLocalMap并将值设置为value。 4.4 remove()方法
源码如下
public void remove() {//根据当前线程获取ThreadLocalMap对象ThreadLocalMap m getMap(Thread.currentThread());if (m ! null)//存在执行remove方法m.remove(this);
}
解释 根据当前线程获取ThreadLocalMap对象存在则执行remove()方法。remove(this)方法中将ThreadLocal作为key来删除对应的Entry。 五、内存泄漏问题
5.1 分析
读到这相信你对ThreadLocal的基本原理有了更深一步的理解我们把上图补全从堆栈视角看一下它们之间的引用关系。 我们可以看到ThreadLocal对象有两个引用一个是栈上的ThreadLocal引用一个是ThreadLocalMap中Key对它的引用。如果栈上的ThreadLocal引用不再使用了那么ThreadLocal对象因为还有一条引用链在所以会导致它无法回收久而久之就会OOM。
这就是我们所说的ThreadLocal的内存泄漏问题为了解决这个问题ThreadLocalMap使用了弱引用就是上述我们说过的Entry数组
static class Entry extends WeakReferenceThreadLocal? {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}
}
可以看出ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法那么可以理解为ThreadLocalMap中的键是ThreadLocal的弱引用。
穿插一下Java中的四大引用 强引用Java中默认的引用类型只要引用还存在即便OOM也不会被回收。 软引用内存不足时将会被干掉。 弱引用无论内存充足与否只要执行GC就会被干掉。 虚引用最弱的一种引用存在意义就是为了将关联虚引用的对象在被GC掉之后收到一个通知。 如果用了弱引用那么ThreadLocal对象就可以在下次GC的时候被回收掉了。 这样做可以很大程度上避免了因为ThreadLocal的使用而导致的OOM问题但也无法彻底避免。
我们可以看到虽然key是弱引用但是value是强引用而且它的生命周期是和Thread一样的也就是说只要Thread还在那么这个对象就无法被回收。
那么什么情况下Thread会一直在呢那就是线程池这就导致value一直无法被回收。
5.2 如何解决
ThreadLocalMap底层使用数组来保存元素使用“线性探测法”来解决hash冲突在每次调用ThreadLocal的get、set、remove方法时内部会实际调用ThreadLocalMap的get、set、remove等操作而ThreaLocalMap的每次set、get、remove时都会对key为null的Entry进行清除expungeStateEntry()方法将Entry的value清空等下次GC就会被回收。
所以当我们一个ThreadLocal用完后就手动remove一下就可以在下次GC时把Entry清理掉。
5.3 总结
上述我们分了两种情况来看ThreadLocal内存泄漏问题 key使用强引用引用ThreadLocal的对象被回收了但是ThreadLocalMap持有ThreadLocal的强引用如果没有手动removeThreadLocal不会被回收导致Entry内存泄漏。 key使用弱引用引用ThreadLocal被回收由于ThreadLocalMap持有ThreadLocal的弱引用即使没有手动removeThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。 比较两种情况我们可以发现由于ThreadLocalMap的生命周期跟Thread一样长如果都没有手动remove就会导致内存泄漏但是使用弱引用可以多一层保障弱引用ThreadLocal被清理后key为null对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
因此ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期和Thread一样长如果没有手动remove就会导致内存泄漏而不是因为弱引用。
End希望对大家有所帮助如果有纰漏或者更好的想法请您一定不要吝啬你的赐教。