TheadLocal是否会导致内存溢出?

最近从网上看到一个关于ThreadLocal的问题,
TheadLocal是否会导致内存溢出?
从理论上说是会的,但是要看怎么使用,既然Java设计了这个东西,肯定是有考虑过它的很多使用场景的,所以大部分情况下其实还是可以放心使用的。

在回答会不会导致OOM之前我们先来了解一下什么是ThreadLocal?通过英文字义,很容易就可以猜到它作用就是用来保存线程的本地变量。

1.下面我们来分析一下ThreadLocal的原理
我们先来看ThreadLocal的get(),set()方法的源码(通过源码去分析它的原理是最直接的)

 public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //把this(ThreadLocal)当作map的Key
            map.set(this, value);
        else
            createMap(t, value);
    }
public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
             //把this(ThreadLocal)当作Key,拿到对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}

 ThreadLocalMap getMap(Thread t) {
        //从当前的线程对象里面拿出ThreadLocalMap对象
        return t.threadLocals;
    }

通过源码可以发现,TheadLocal.set的value保存的位置不是在TheadLocal里面,而是在当前线程的ThreadLocalMap里面,并且ThreadLocalMap的key是ThreadLocal本身。当调用get()的通过传入ThreadLocal本身就可以拿到对应的Entry。接下来我们看到Entry的实现,它继承了WeakReference类
(WeakReference是Java的弱引用,每次调用GC的时候如果某个对象只存在弱应用,那么它一定会被回收掉)。

下面是ThreadLocalMap 的Entry 的实现

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        。。。
  }

在Entry中,将key传入了WeakReference的构造函数,也就是说只有Key是弱引用,value是强引用,而threadLocalMap对Entry的引用也是强引用。(如下图)
在这里插入图片描述

由于Key是弱引用,也就是说,当threadLocalMap中的key只要在外面不存在任何强引用的情况threadLocalMap中的key是会被回收的,那key回收了之后有什么作用呢?我们再来看一下threadLocalMap的set方法实现

 private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                //当key为null时,将key为null的entry的引用去掉
                if (k == null) {                    
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

通过上面的代码,我们可以看出来,调用set方法时每次都会去查询threadLocalMap的entry,如果存在key为空时,它的entry的将会被清除掉。(调用threadLocalMap的get,remove方法也会)

所以通过上面的代码分析之后,我们得出了两个结论:
1.threadlocal变量是缓存在thread对象里面,threadlocal只是做key来使用,那么如果thread给回收了,那么threadlocal的变量缓存也会被回收。
2.threadLocalMap对threadlocal采用的是弱引用, threadlocal在外面不存在强引用时,只要有其他的方法调用了的threadLocalMap的get,set或remove方法,它会清除掉一些已经无效的threadlocal的缓存。

我们知道要导致OOM就得必须不停的创建对象,并且创建的对象不会被回收掉。那么要threadlocal的缓存不被回收,那需要满足以下两点:
1.首先调用Threadlocal的线程,寿命必须要有足够长(这个是有可能的,很多线程池的线程都会重复使用)
2.如果只是一个threadlocal对象是很难导致OOM的,假如我们的应用程序有1000个线程,那最多这个threadlocal对象使用的本地缓存变量最多也1000个,现在的服务器那么多的内存,这个占用还不至于导致内存泄漏,所以我们需要非常泛滥的使用threadlocal变量,而且必须是静态的,因为静态变量的引用才会永久存在。

分析完了上面的原理之后,我们再来分析一下实际中使用的场景

场景1

class BizContext {
    private ThreadLocal<Object> local = new ThreadLocal<>();
    public void setVal(Object val) {
         local.set(val);
    }
    public Object getVal() {
        return local.get();
    }
}
class TestBiz1 {
    BizContext context;

    public TestBiz1(BizContext context) {
        this.context = context;
    }
    public void doSomething() {
        BizContext ctx = this.context;
        Random random = new Random();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    ctx.setVal(random.nextInt());
                    TestBiz2 testBiz2=new TestBiz2(context);
                    testBiz2.doSomething();
                }
            });
            thread.start();
        }
    }
}
class TestBiz2 {
    BizContext context;
    public TestBiz2(BizContext context) {
        this.context = context;
    }

    public void doSomething() {
       System.out.println(context.getVal());
    }
}

class Test {
    public static void main(String[] arr) {
        Random random = new Random();
        for (int i = 0; i < 1000000; i++) {
            BizContext context = new BizContext();
            TestBiz1 testBiz1 = new TestBiz1(context);
            testBiz1.doSomething();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

这种场景不会导致内存溢出,为什么呢?
1.首先上面例子每次调用testBiz1.doSomething(),都会创建1000个线程,但是线程的寿命都很短,线程使用完之后就会被回收(我们先不管这样使用性能的好坏),所以ThreadLocal的缓存也会跟着一起回收。
2.在main方法的for循环里面,每次调用调用完testBiz1.doSomething()方法之后,BizContext 都是新实例化的,旧的BizContext 实例它没有被任何对象引用,所以它和里面的ThreadLocal都会被回收掉,所以即使调用的线程即使调用完之后没有死,在调用它的ThreadLocalMap的set方法时,引用旧的ThreadLocal的entry也会被回收掉。

场景二

class BizContext {
public static ThreadLocal<Object> local1=new ThreadLocal<>();
public static ThreadLocal<Object> local2=new ThreadLocal<>();
public static ThreadLocal<Object> local3=new ThreadLocal<>();
public static ThreadLocal<Object> local4=new ThreadLocal<>();
public static ThreadLocal<Object> local5=new ThreadLocal<>();
public static ThreadLocal<Object> local6=new ThreadLocal<>();
public static ThreadLocal<Object> local7=new ThreadLocal<>();
....
public static ThreadLocal<Object> local10000=new ThreadLocal<>();
}
class Test{
   public static void main(String[] arr) {
       ExecutorService executorService = new ThreadPoolExecutor(1000, 1000,
                0, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1000000), 
                new ThreadPoolExecutor.DiscardPolicy());
   }
   for (; ; ) {
            Person person = new Person();
            executorService.execute(() -> {
                   BizContext.local1.set(1);
                   BizContext.local2.set(2);
                   BizContext.local3.set(3);
                   BizContext.local4.set(4);
                   BizContext.local5.set(5);
                   ......
                   BizContext.local10000.set(10000);
            });
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
   }
}

上面这种场景模拟的是ThreadLocal变量泛滥的情况 (虽然有点夸张,没有人会可能这样用,但是这里只是为了举例),这种是可能会发生内存溢出的

结论
ThreadLocal的设计是非常合理的,并不是说用了它就一定会导致内存溢出,正常的使用是不用担心的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章