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的設計是非常合理的,並不是說用了它就一定會導致內存溢出,正常的使用是不用擔心的。

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