ThreadLocal真会内存泄漏?

打印 上一主题 下一主题

主题 834|帖子 834|积分 2502

前言

在讨论ThreadLocal存在内存泄漏标题之前,必要先了解下面几个知识点:

  • 什么是内存泄漏?
  • 什么是ThreadLocal?
  • 为什么必要ThreadLocal?

    • 数据同等性标题
    • 怎样解决数据同等性标题?

当我们了解了上面的知识点以后,会带大家一起去了解真相。包罗下面几个知识点:

  • 为什么会产生内存泄漏?
  • 实战复现标题
  • 怎样解决内存泄漏?
  • 为什么是弱引用?
只有了解上面的知识点,才能更好的明白以及怎样解决ThreadLocal内存泄漏标题。下面我们就开始带大家一步一步的去了解。
什么是内存泄漏?

在讨论ThreadLocal存在内存泄漏标题之前,我觉得有必要先了解一下什么是内存泄漏?我们为什么要解决内存泄漏的标题?这里引用一段百度百科对内存泄漏的解释。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成体系内存的浪费,导致程序运行速度减慢甚至体系崩溃等严峻后果。
从Java的内存管理来说,就是ThreadLocal存在无法被GC接纳的内存。这些无法被接纳的内存,假如随着时间的推移,从而导致超出内存容量「内存溢出」,最终导致程序崩溃「OutOfMemoryError」。所以为了克制我们的Java程序崩溃,我们必须要克制出现内存泄漏的标题。
ThreadLocal

前面讲了什么是内存泄漏,为什么要解决内存泄漏的标题。现在我们来讲讲什么是ThreadLocal?
❝简单来说,ThreadLocal是一个本地线程副本变量工具类。ThreadLocal让每个线程有自己”独立“的变量,线程之间互不影响。ThreadLocal为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。
为什么必要ThreadLocal?

现在我们知道了什么是ThreadLocal,接下来我们讲讲为什么必要ThreadLocal在讲为什么必要ThreadLocal之前,我们必要了解一个标题。那就是数据同等性标题。由于ThreadLocal就是解决数据同等性标题的一种方案,只要当我们了解什么是数据同等性标题后,自然就知道为什么必要ThreadLocal了。
什么是同等性标题?

多线程充分利用了多核CPU的本领,为我们程序提供了很高的性能。但是有时候,我们必要多个线程互相协作,这里可能就会涉及到数据同等性的标题。 数据同等性标题指的是:发生在多个主体对同一份数据无法达成共识。
怎样解决同等性标题?


  • 「列队」:假如两个人对一个标题的看法不同等,那就排成一队,一个人一个人去修改它,这样背面一个人总是能够得到前面一个人修改后的值,数据也就总是同等的了。Java中的互斥锁等概念,就是利用了列队的思想。列队虽然能够很好的确保数据同等性,但性能非常低。
  • 「投票」:,投票的话,多个人可以同时去做一件决策,大概同时去修改数据,但最终谁修改成功,是用投票来决定的。这个方式很高效,但它也会产生很多标题,好比网络中断、敲诈等等。想要通过投票到达同等性非常复杂,通常必要严格的数学理论来证明,还必要中心有一些“信使”不断来往返回传递消息,这中心也会有一些性能的开销。我们在分布式体系中常见的Paxos和Raft算法,就是使用投票来解决同等性标题的。
  • 「克制」:既然包管数据同等性很难,那我能不能通 过一些手段,去克制多个线程之间产生同等性标题呢?我们熟悉的Git就是这个实现,大家在本地分布式修改同一个文件,通过版本控制和解决冲突去解决这个标题。而ThreadLocal也是使用的这种方式。
为什么会产生内存泄漏?

上面讲清楚了ThreadLocal的根本含义,接下来我们一起看看ThreadLocal常用函数的源码,只有了解ThreadLocal的具体实现才能更好的资助我们明白它为什么会产生内存泄漏的标题。
set()方法
  1. public void set(T value) {<br>    Thread t = Thread.currentThread(); <br>    ThreadLocalMap map = getMap(t); <br>    if (map != null)<br>        map.set(this, value); <br>    else<br>        createMap(t, value);<br>}<br><br>ThreadLocalMap getMap(Thread t) {<br>    return t.threadLocals;<br>}<br>
复制代码
从上面的源码可以看出,当我们调用ThreadLocal对象的set()方法时,着实就是将ThreadLocal对象存入当火线程的ThreadLocalMap集合中,map集合的key为当前ThreadLocal对象,value为set()方法的参数。
  1. static class ThreadLocalMap {<br>    static class Entry extends WeakReference<ThreadLocal<?>> {<br><br>        Object value;<br><br>        Entry(ThreadLocal<?> k, Object v) {<br>            super(k);<br>            value = v;<br>        }<br>    }<br><br> private Entry[] table;<br>}<br>
复制代码
这是ThreadLocalMap的源码(由于篇幅原因这里我只取了紧张的代码),可以看到ThreadLocalMap中使用一个Entry对象来存储数据,而Entry的key则是一个WeakReference弱引用对象。这里我带大家再复习一下Java对象的几种引用。

  • 「强引用」:java中的引用默认就是强引用,任何一个对象的赋值操作就产生了对这个对象的强引用。如:Object o = new Object(),只要强引用关系还在,对象就永久不会被接纳。
  • 「软引用」:java.lang.ref.SoftReference,JVM会在内存溢出前对其进行接纳。
  • 「弱引用」:java.lang.ref.WeakReference,不管内存是否够用,下次GC肯定接纳。
  • 「虚引用」:java.lang.ref.PhantomReference,也称“幽灵引用”、“幻影引用”。虚作用是跟踪垃圾接纳器收集对象的活动,在GC的过程中,假如发现有PhantomReference,GC则会将引用放到ReferenceQueue中,由程序员自己处置惩罚,当程序员调用ReferenceQueue.pull()方法,将引用出ReferenceQueue移除之后,Reference对象会变成Inactive状态,意味着被引用的对象可以被接纳了,虚引用的唯一的目的是对象被接纳时会收到一个体系通知。
实战复现标题

上面我们已经了解了ThreadLocal存储数据的set()方法,现在我们来看一段代码,通过代码来分析ThreadLocal为什么会产生内存泄漏。
  1. public class Test {<br><br>    @Override<br>    protected void finalize() throws Throwable {<br>        System.err.println("对象被回收了");<br>    }<br>}<br>
复制代码
  1. @Test<br>void test() throws InterruptedException {<br>    ThreadLocal<Test> local = new ThreadLocal<>();<br>    local.set(new Test());<br>    local = null;<br>    System.gc();<br>    Thread.sleep(1000000);<br>}<br>
复制代码
我们创建一个测试类,并重写finalize()方法,当对象被接纳时会打印消息在控制台方便我们测试观察对象是否被接纳。
从代码可以看到,我们创建了一个ThreadLocal对象,然后往对象里面设置了一个new Test对象,然后我们将变量local赋值为null,最后手动触发一下gc。大家可以猜猜,控制台会打印出对象被接纳了的消息吗?发起大家动手试试,增加一下明白。
在告诉大家答案之前我们先来分析一下上面的一个引用关系:
示例中local = null这行代码会将强引用2断掉,这样new ThreadLocal对象就只有一个弱引用4了,根据弱引用的特点在下次GC的时候new ThreadLocal对象就会被接纳。那么new Test对象就成了一个永久无法访问的对象,但是又存在一条强引用链thread→Thread对象→ThreadLocalMap→Entry→new Test,假如这条引用链不停存在就会导致new Test对象永久不会被接纳。由于现在大多时候都是使用线程池,而线程池会复用线程,就很容易导致引用链不停存在,从而导致new Test对象无法被接纳,一旦这样的情况随着时间的推移而大量存在就容易引发内存泄漏。
怎样解决内存泄漏?

我们已经知道了造成内存泄漏的原因,那么要解决标题就很简单了。
上面造成内存泄漏的第一点就是Entry的key也就是new ThreadLocal对象的强引用被断开了,我们就可以想办法让这条强引用无法断开,好比将ThreadLocal对象设置为private static 包管任何时候都能访问new ThreadLocal对象同时克制其他地方将其赋值为null。
还有一种办法就是想办法将new Test对象接纳,从根本上解决标题。下面我们一起看看ThreadLocal为我们提供的方法。
remove()方法
  1. public void remove() {<br>   ThreadLocalMap m = getMap(Thread.currentThread());<br>   if (m != null)<br>       m.remove(this);<br>}<br>private void remove(ThreadLocal<?> key) {<br>  Entry[] tab = table;<br>  int len = tab.length;<br>  int i = key.threadLocalHashCode & (len-1);<br>  for (Entry e = tab[i];<br>       e != null;<br>       e = tab[i = nextIndex(i, len)]) {<br>      if (e.get() == key) {<br>          e.clear();<br>          expungeStaleEntry(i);<br>          return;<br>      }<br>  }<br>}<br>private int expungeStaleEntry(int staleSlot) {<br>      Entry[] tab = table;<br>      int len = tab.length;<br><br>      // expunge entry at staleSlot<br>      tab[staleSlot].value = null;<br>      tab[staleSlot] = null;<br>      size--;<br>   // 省略代码...感兴趣可以去看看源码<br>      return i;<br>  }<br>
复制代码
该方法的逻辑是,将entry里value的强引用3和key的弱引用4置为null。这样new Test对象和Entry对象就都能被GC接纳。
因此,只要调用了expungeStaleEntry() 就能将无用 Entry 接纳扫除掉。
但是该方法为private故无法直接调用,但是ThreadLocalMap中remove()方法直接调用了该方法,因此只要当我们使完ThreadLocal对象后调用一下remove()方法就能克制出现内存泄漏了。
综上所述:针对ThreadLocal 内存泄漏的原因,我们可以从两方面去考虑:

  • 删除无用 Entry 对象。即 用完ThreadLocal后手动调用remove()方法。
  • 可以让ThreadLocal对象的强引用不停存在,包管任何时候都可以访问到 Entry的 value值。即 将ThreadLocal 变量界说为 private static。
为什么是弱引用?

不知道大家有没有想过一个标题,既然是弱引用导致的内存泄漏,那么为什么JDK还要使用弱引用。难道是bug吗?大家再看一下下面这段代码。
  1. @Test<br>void test() throws InterruptedException {<br>    ThreadLocal<Test> local = new ThreadLocal<>();<br>    local.set(new Test());<br>    local = null;<br>    System.gc();<br>    Thread.sleep(1000000);<br>}<br>
复制代码
我们假设Entrykey使用强引用,那么引用图就是如下
当代码local = null断掉强引用2的时候,new ThreadLocal对象就是只存在一条强引用4,那么由于强引用的关系GC无法接纳new ThreadLocal对象。所以就造成了Entry的key和value都无法访问无法接纳了,内存泄漏就加倍了。
同理也不能将Entry的value设置为弱引用,由于Entry对象的value即new Test对象只有一个引用,假如使用弱引用,在GC的时候会导致new Test对象被接纳,导致数据丢失。
将Entry的key设置为弱引用还有一个长处就是,当强引用2断掉且弱引用4被GC接纳后,ThreadLocal会通过key.get() == null识别出无用Entry从而将Entry的key和value置为null以便被GC接纳。具体代码如下
  1. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {<br>    Entry[] tab = table;<br>    int len = tab.length;<br><br>    while (e != null) {<br>        ThreadLocal<?> k = e.get();<br>        if (k == key)<br>            return e;<br>        if (k == null)<br>            expungeStaleEntry(i);<br>        else<br>            i = nextIndex(i, len);<br>        e = tab[i];<br>    }<br>    return null;<br>}<br>
复制代码
所以,Entry key使用弱引用并不是一个bug,而是ThreadLocal的开发人员在尽力的资助我们克制造成内存泄漏。
彩蛋

[code]@Test
void test2() throws InterruptedException {
    ThreadLocal local = new ThreadLocal();
  local.set(new Test());
    local = null;
    System.gc();
   for (int i = 0; i  key) { key, int i, Entry e) { k = e.get();

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表