![23e5edae9a8bbb4f505df55695649baa.png](https://i-blog.csdnimg.cn/blog_migrate/3b73470ab976cc7ee9832605cc08f069.png)
![23e5edae9a8bbb4f505df55695649baa.png](https://i-blog.csdnimg.cn/blog_migrate/3b73470ab976cc7ee9832605cc08f069.png)
![9c0306244430b45680845e9004bad827.png](https://i-blog.csdnimg.cn/blog_migrate/13c1665527207da827371d23dbf230de.jpeg)
常见面试点
- 如何避免ThreadLocal导致内存泄露
ThreadLocal的实现原理与应用情景
ThreadLocal结合线程池使用的问题
什么是ThreadLocal
JDK源码中是这样描述的:ThreadLocal提供了线程局部变量,而线程局部变量与普通变量不同,它是每个线程都有一个变量的副本相互隔离,通过get或set方法操作线程对应的变量。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.
ThreadLocal提供了一种线程安全问题的新解决方案,通过以空间换时间的方式保存变量副本到每个线程,防止线程之间对共享变量的读写冲突,相比synchronized关键字来解决共享资源的冲突更加轻便、简洁。
内部结构及原理
通过观察类图及关系引用可以发现每个Thread里包含对ThreadLocalMap的引用,而ThreadLocalMap是ThreadLocal的内部类,在ThreadLocalMap内部里拥有Entry[]数组,正是通过Entry数组来保存每个线程与变量之间的关联。
//ThreadLocalMap内部静态类,/* Entry继承弱引用的原因是当ThreadLocal对象无强引用时,ThreadLocalMap里的Entry对应的Key就没有强引用,在JVM GC时就可以回收线程中关联的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; }}
- 强引用(Strong Reference):代码中引用了new Object()构建的对象都属于强引用,即使虚拟机内存不足时,宁愿抛出OutOfMemoryError异常,也不会回收强引用的对象。
- 软引用(Soft Reference):当虚拟机内存充足时触发GC不会回收软引用对象,但当内存不足且触发GC时则会回收软引用对象及占用的内存。
- 弱引用(Weak Reference):当GC触发时不管虚拟机内存是否充足,都会回收弱引用对象及内存。
- 虚引用(Phantom Reference):虚引用并不决定对象的生命周期,一个对象持有虚引用与没持有差不多,主要跟踪垃圾回收时对象的记录。
注意:当线程副本引用被置为null后,ThreadLocal对象就没有强引用存在,在JVM发生GC时可对Entry的key对象(即ThreadLocal)回收,但建议在使用完threadLocal后通过remove方法把Entry数组中的内容移除,避免发生内存泄露。
ThreadLocal类提供以下几个核心方法:
T get():获取线程的变量副本值
void set(T value):设置线程变量副本值
void remove():删除线程的变量副本
get()方法
获取当前用的线程,并找到线程关联的threadLocalMap
当threadLocalMap为空时初始化一个新的并直接返回,否则根据key查找对应的Entry并返回值
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //若当前线程关联的ThreadLocal不为空则查询 if (map != null) { //根据threadLocal查询对应的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}privateT setInitialValue() { //默认返回null值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //如果当前调用线程关联的ThreadLocalMap为空则创建,否则设置值进去 if (map != null) map.set(this, value); else //new ThreadLocalMap(this,value) createMap(t, value); return value;}private Entry getEntry(ThreadLocal> key) { //根据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);}private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //数组下标的Entry不为空且关联的threadlocal与查找的threadlocal不一致 while (e != null) { ThreadLocal> k = e.get(); //entry关联的threadlocal与查找的相等则直接返回 if (k == key) return e; if (k == null) //关联的threadlocal为空,则触发清理key为null的Entry并重新进行rehash旧Entry数组的元素 //threadLocalMap的hash冲突与hashMap的冲突处理方式不一致,hashMap使用的是链表地址法, //而threadLocalMap使用的开放地址法——线性探测,即顺序查找下一位置或者遍历全表,效率较低 expungeStaleEntry(i); else //递增下标i的值进行下一轮的查找 i = nextIndex(i, len); e = tab[i]; } return null;}
小插曲:hash冲突的解决方法:
开放地址法:当关键字的哈希地址N出现冲突时,以冲突地址为基础生成另一个哈希地址N1,如果N1仍然冲突,再以其为基础,生成另一个新的哈希地址N2,直到找出不冲突的哈希地址并将相应元素存入
-
线性探测再散列:冲突发生时顺序查找下一个地址,直到找出一个空的或查遍全表,如ThreadLocalMap
二次探测再散列:冲突发生时在表的左右进行跳跃式探测
伪随机探测再散列:当发生冲突时,通过加入随机数2、3、5、9等作为偏移量定位下一个地址
再哈希法:构造多个哈希函数且当关键字哈希地址冲突时执行下一个hash函数,直至不在出现地址冲突为止
链地址法:把有哈希地址冲突的关键字构建成链表结构,适用于删除、插入较多情景,如HashMap
公共溢出区:这种方式的思想是通过建立基础区、溢出区,没冲突的放到基础区,冲突的放到溢出区
set(T value)方法
获取当前用的线程,并找到线程关联的threadLocalMap
若threadLocalMap不为空则把更新对应的值,否则创建一个新的
public void set(T value) { //1、获取到当前调用线程 Thread t = Thread.currentThread(); //2、获取当前线程中的ThreadLocalMap并是否为空,若不为空则调用set方法,否则创建新的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}ThreadLocalMap getMap(Thread t) { return t.threadLocals;}void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}
remove()方法
获取当前用的线程,并找到线程关联的threadLocalMap
若threadLocalMap不为空则删除关联的值,否则啥也不做
//ThreadLocalpublic void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //删除当前threadLocal对象关联的Entry m.remove(this);}//ThreadLocalMapprivate 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; } }}
应用情景
大名鼎鼎的Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果,还有大家耳闻目睹的ORM框架Mybatis同样也是用ThreadLocal管理SqlSession。
//Spring TransactionSynchronizationManager类@Overrideprotected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { //此处省略N行代码 if (txObject.isNewConnectionHolder()) { //绑定数据库连接到线程中 TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { //当发生异常时,移除线程中的连接 DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); }}
通常我们是使用如下的方式操作ThreadLocal,在操作完后一定要remove掉线程关联的Entry,防止内存泄露。
private static final ThreadLocal loginUserLocal = new ThreadLocal();public static LoginUser getLoginUser() { return loginUserLocal.get();}public static void setLoginUser(LoginUser loginUser) { loginUserLocal.set(loginUser);}public static void clear() { loginUserLocal.remove();}//在使用完后一定要清理防止内存泄露try{ loginUserLocal.set(loginUser); //执行其他业务逻辑}finally{ loginUserLocal.remove();}
总结
ThreadLocal提供一种多线程并发安全的新解决方案,通过以空间换取时间及线程的变量副本隔离来实现安全
ThreadLocalMap里的Entry类使用虚引用作为key,当threadLocal对象没有强引用时,GC便会回收调threadLocal对象,但此时扔需要remove清理Entry类的value
每个ThreadLocal中只能只能保存一个变量副本,若需要保存多个变量副本则需要建立多个ThreadLocal
当结合线程池使用ThreadLocal时,一定要在使用完后执行remove操作,否则会导致下一个线程再次使用ThreadLocal发现遗留历史的数据在Entry里,导致业务错误等错误
![11d26265c16273269ebd50a2234f9952.png](https://i-blog.csdnimg.cn/blog_migrate/b014c28a1cbe809406ae105eeca1b2b9.png)