Java面試題:細數ThreadLocal大坑,記憶體洩露本可避免

猫鱼吐泡泡發表於2024-04-22

一、背景
ThreadLocal是Java中用於解決多執行緒共享變數導致的執行緒安全問題的一種機制。它為每個執行緒分配一個獨立的變數副本,從而避免了執行緒間的資料競爭。這個我們從上一篇文章《Java面試題:請談談對ThreadLocal的理解?》中已經瞭解。然而,如果使用不當,ThreadLocal也可能導致記憶體洩露。

那什麼是記憶體洩漏,它和記憶體溢位有什麼區別?

  • 記憶體溢位(Memory overflow):沒有足夠的記憶體提供申請者使用。

  • 記憶體洩漏(Memory leak):指程式申請記憶體後,無法釋放已申請的記憶體空間,記憶體洩漏的堆積終將導致記憶體溢位。

二、記憶體洩露案例

以下是一個可能導致ThreadLocal記憶體洩露的程式碼示例:

public class ThreadLocalMemoryTest {
    // 定義一個靜態的ThreadLocal變數
    private static final ThreadLocal<LeakyObject> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 建立一個物件並且儲存到ThreadLocal中
        threadLocal.set(new LeakyObject());

        // 強制進行垃圾回收,以便我們可以看到物件是否被回收
        System.gc();

        try {
            // 等待垃圾回收器進行垃圾回收
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 列印MemoryLeakyObject是否被回收的資訊
        if (LeakyObject.isInstanceActive()) {
            System.out.println("LeakyObject instance is still active!");
        } else {
            System.out.println("LeakyObject instance has been garbage collected.");
        }

        while(true) {

        }
    }

    // 一個簡單的記憶體洩漏示例類
    private static class LeakyObject {
        private static boolean instanceActive = true;

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            instanceActive = false;
            System.out.println("LeakyObject finalize method has been called.");
        }

        public static boolean isInstanceActive() {
            return instanceActive;
        }
    }
}

執行結果如下:

LeakyObject instance is still active!

在這個例子中,我們定義了一個LeakyObject類,它有一個靜態變數instanceActive來跟蹤物件例項是否仍然存在。重寫的finalize方法會在物件被垃圾回收器回收時被呼叫,並將instanceActive設定為false。

三、程式碼最佳化與記憶體洩露避免

為了解決這個問題,我們需要確保在不再需要ThreadLocal中的資料時釋放其佔用的記憶體,從而避免記憶體洩露。在System.gc();程式碼執行之前,模擬清除ThreadLocal中的資料。

// 模擬執行緒退出,應該清除ThreadLocal中的資料
threadLocal.remove();
// 強制進行垃圾回收,以便我們可以看到物件是否被回收
System.gc();

執行結果如下:

LeakyObject finalize method has been called.
LeakyObject instance has been garbage collected.

ThreadLocalMemoryLeakTest類中的main方法模擬了ThreadLocal的使用,並在使用後呼叫remove方法來清除ThreadLocal中的資料,然後強制進行垃圾回收並等待一段時間,最後檢查物件是否被垃圾回收器回收。

四、總結

ThreadLocal作為一種解決多執行緒共享變數問題的機制,在正確使用的情況下可以提供很高的效能和可靠性。然而,如果不正確使用,它也可能導致記憶體洩露。
透過了解並避免上述案例中的問題,我們可以更好地利用ThreadLocal來提高應用程式的效能和可靠性。在本次案例中,我們是透過ThreadLocal.remove()方法,來解決記憶體洩漏問題。


但是其實解決ThreadLocal記憶體洩漏問題的方法還有2種,需要依據不同的使用場景:

  • 使用不可變物件:ThreadLocal變數儲存的物件最好是不可變的,因為不可變的物件不需要頻繁更新,也不會因為被多個執行緒同時修改而出現執行緒安全問題。如果要修改一個ThreadLocal變數中的物件,最好使用一個新的物件替換原有的物件,從而避免引用洩漏的問題。
  • 使用弱引用:ThreadLocalMap中的弱引用可以保證ThreadLocal例項在當前執行緒中不再被引用時能夠被GC回收,從而防止記憶體洩漏問題的發生。

相關文章