为什么ThreadLocal容易导致内存泄漏?

开发 前端
我们通常创建的变量可以被任何线程访问和修改,而是用ThreadLocal创建的变量只能通过当前线程去访问和修改。

[[416507]]

本文转载自微信公众号「三不猴子」,作者sanbuhouzi。转载本文请联系三不猴子公众号。

为什么ThreadLocal容易导致内存泄漏?

ThreadLocal是什么?

官方解释为:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

我们通常创建的变量可以被任何线程访问和修改,而是用ThreadLocal创建的变量只能通过当前线程去访问和修改。

ThreadLocal原理

jdk版本1.8,我们先看一下ThreadLocal的源码,先从set方法开始。

  1. /** 
  2.    * Sets the current thread's copy of this thread-local variable 
  3.    * to the specified value.  Most subclasses will have no need to 
  4.    * override this method, relying solely on the {@link #initialValue} 
  5.    * method to set the values of thread-locals. 
  6.    * 
  7.    * @param value the value to be stored in the current thread's copy of 
  8.    *        this thread-local
  9.    */ 
  10.   public void set(T value) { 
  11.       Thread t = Thread.currentThread(); 
  12.       ThreadLocalMap map = getMap(t); 
  13.       if (map != null
  14.           map.set(this, value); 
  15.       else 
  16.           createMap(t, value); 
  17.   } 

这个ThreadLocalMap是ThreadLocal的一个内部类,key是当前Thread对象,value是我们要存的对象。首先拿到当前线程对象,然后获取了个map,然后往这个map中放了当前ThreadLocal对象,如果map为空则创建一个map。看看getMap的逻辑。

  1. /** 
  2.     * Get the map associated with a ThreadLocal. Overridden in 
  3.     * InheritableThreadLocal. 
  4.     * 
  5.     * @param  t the current thread 
  6.     * @return the map 
  7.     */ 
  8.    ThreadLocalMap getMap(Thread t) { 
  9.        return t.threadLocals; 
  10.    } 

getMap就是在Thread成员变量中获取一个map。往下就是ThreadLocalMap.set()看看set的逻辑。

  1. /** 
  2.      * Set the value associated with key
  3.      * 
  4.      * @param key the thread local object 
  5.      * @param value the value to be set 
  6.      */ 
  7.     private void set(ThreadLocal<?> key, Object value) { 
  8.  
  9.         // We don't use a fast path as with get() because it is at 
  10.         // least as common to use set() to create new entries as 
  11.         // it is to replace existing ones, in which case, a fast 
  12.         // path would fail more often than not
  13.  
  14.         Entry[] tab = table
  15.         int len = tab.length; 
  16.         int i = key.threadLocalHashCode & (len-1); 
  17.  
  18.         for (Entry e = tab[i]; 
  19.              e != null
  20.              e = tab[i = nextIndex(i, len)]) { 
  21.             ThreadLocal<?> k = e.get(); 
  22.  
  23.             if (k == key) { 
  24.                 e.value = value; 
  25.                 return
  26.             } 
  27.  
  28.             if (k == null) { 
  29.                 replaceStaleEntry(key, value, i); 
  30.                 return
  31.             } 
  32.         } 
  33.  
  34.         tab[i] = new Entry(key, value); 
  35.         int sz = ++size
  36.         if (!cleanSomeSlots(i, sz) && sz >= threshold) 
  37.             rehash(); 
  38.     } 

这里构造了个Entry对象,这个Entry可以看成是map的一行数据,一个key-value对。再看看Entry的源码。

  1. static class Entry extends WeakReference<ThreadLocal<?>> { 
  2.             /** The value associated with this ThreadLocal. */ 
  3.             Object value; 
  4.  
  5.             Entry(ThreadLocal<?> k, Object v) { 
  6.                 super(k); 
  7.                 value = v; 
  8.             } 
  9.         } 

这个Entry对象竟然是继承了WeakReference对象。上面的流程用图画出来就是这样的。

总结起来就是:

  • 每个Thread维护着一个ThreadLocalMap的引用
  • ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  • 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,value是传递进来的对象
  • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

什么是弱引用呢? 为什么ThreadLocal要使用弱引用呢?

官方文档解释为:

  1. /** 
  2.  * Weak reference objects, which do not prevent their referents from being 
  3.  * made finalizable, finalized, and then reclaimed.  Weak references are most 
  4.  * often used to implement canonicalizing mappings. 
  5.  * 
  6.  * <p> Suppose that the garbage collector determines at a certain point in time 
  7.  * that an object is <a href="package-summary.html#reachability">weakly 
  8.  * reachable</a>.  At that time it will atomically clear all weak references to 
  9.  * that object and all weak references to any other weakly-reachable objects 
  10.  * from which that object is reachable through a chain of strong and soft 
  11.  * references.  At the same time it will declare all of the formerly 
  12.  * weakly-reachable objects to be finalizable.  At the same time or at some 
  13.  * later time it will enqueue those newly-cleared weak references that are 
  14.  * registered with reference queues. 
  15.  * 
  16.  * @author   Mark Reinhold 
  17.  * @since    1.2 
  18.  */ 

就是不会被程序计数器计数的引用,所以在垃圾回收器回收的时候不管是否有引用都会被回收。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

ThreadLocal为什么要使用弱引用?

因为当我们存入的对象被置为null的时候,也就是ThreadLocalMap的value为null,ThreadLocalMap的key是弱引用此时在下一次垃圾回收器回收垃圾的时候就可以回收掉这个key-value也是就一个Entry对象了。

既然弱引用是有助于垃圾回收的,那为什么ThreadLocal还是容易导致内存泄漏?

弱引用确实是有助于垃圾回收,但是也是有弊端的,假设我们现在存入了一个对象,此时虚拟机gc,将key弱引用回收,但是value依然是强引用,key被回收了,这个value无法通过通过ThreadLocal对象的get方法获取,它永远不会被访问到了,所以存在内存泄漏的风险。

如何避免内存泄漏

  • 在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理。 
  • 尽量不要使用全局的ThreadLocal,静态变量的生命周期和类的生命周期是一致的,而类的卸载时机可以说比较苛刻,这会导致静态ThreadLocal无法被垃圾回收,容易出现内存泄漏。

 

责任编辑:武晓燕 来源: 三不猴子
相关推荐

2018-10-25 15:24:10

ThreadLocal内存泄漏Java

2022-05-09 14:09:23

多线程线程安全

2022-10-18 08:38:16

内存泄漏线程

2020-09-10 07:40:28

ThreadLocal内存

2011-05-24 16:39:09

Cfree()

2021-02-18 16:53:44

内存ThreadLocal线程

2024-03-22 13:31:00

线程策略线程池

2022-07-26 07:14:20

线程隔离Thread

2017-02-27 15:43:51

2019-12-17 10:01:40

开发技能代码

2017-06-02 10:57:29

Android内存泄漏Dialog

2023-05-29 07:17:48

内存溢出场景

2023-10-24 09:30:49

Java内存

2024-03-11 08:22:40

Java内存泄漏

2023-12-18 10:45:23

内存泄漏计算机服务器

2012-02-22 21:28:58

内存泄漏

2020-01-14 10:57:39

内存泄漏虚拟机

2016-05-26 12:11:00

Redis内存开源

2015-03-30 11:18:50

内存管理Android

2022-11-28 09:00:03

编程bug开发
点赞
收藏

51CTO技术栈公众号