Java中的引用與ThreadLocal

SpringDawn發表於2020-08-10

Java中的引用--強軟弱虛

強引用

Object object = new Object(),這個object就是一個強引用。如果一個物件具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError異常,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。

軟引用(SoftReference)

如果一個物件只具有軟引用,那就類似於可有可物的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體。 軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

public class TestSoftReference {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        // m強引用指向softReference,softReference軟指向byte[]
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10],queue);
        // 列印結果:[B@1e643faf
        System.out.println(m.get());
        System.gc();
        Thread.sleep(1000);
        // 列印結果:[B@1e643faf 表示沒有被垃圾回收
        System.out.println(m.get());
        // 給出一個強引用
        byte[] bytes = new byte[1024 * 1024 * 15];
        // 不規定最大堆記憶體大小時,列印結果:[B@1e643faf
        // 指定最大堆記憶體-Xmx20M時,列印輸出null
        System.out.println(m.get());
        //列印結果:java.lang.ref.SoftReference@6e8dacdf
        System.out.println(queue.poll());
    }
}

不指定引數,輸出結果

[B@1e643faf
[B@1e643faf
[B@1e643faf
null

指定引數-Xmx20M,輸出結果

[B@1e643faf
[B@1e643faf
null
java.lang.ref.SoftReference@6e8dacdf

弱引用(WeakReference)

如果一個物件只具有弱引用,那就類似於可有可物的生活用品。弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它 所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒, 因此不一定會很快發現那些只具有弱引用的物件。 弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

public class TestWeakReference {
    public static void main(String[] args) {
        WeakReference<byte[]> m = new WeakReference<>(new byte[1024*1024*10]);
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
    }
}

有垃圾回收直接回收,列印結果:

[B@1e643faf
null

虛引用(PhantomReference)

顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。 虛引用主要用來跟蹤物件被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。 主要用在管理對外記憶體

ThreadLocal

ThreadLocal提供執行緒區域性變數。這些變數與普通變數不同,因為每個執行緒都有其自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態變數,並將它與執行緒的狀態繫結(例如,使用者ID或事務ID)。

簡單案例:

public class TestThreadLocal {
    private static final AtomicInteger nextId = new AtomicInteger(0);

    private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement);

    public static int get() {
        return threadId.get();
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(TestThreadLocal.get());   // 0
            try {
                Thread.sleep(1000);
                System.out.println(TestThreadLocal.get());   // 0
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{System.out.println(TestThreadLocal.get());}).start();   // 1
    }
}

這裡通過ThreadLocal物件threadId為每一個呼叫TestThreadLocal.get()方法的執行緒賦予一個執行緒Id,第4行通過ThreadLocal.withInitial(nextId::getAndIncrement)得到ThreadLocal的子類SuppliedThreadLocal物件,SuppliedThreadLocal物件複寫了initialValue方法。

        @Override
        protected T initialValue() {
            return supplier.get();
        }

具體細節下面再談。先看看main方法,其中啟動了兩個執行緒,可以看到每個執行緒通過呼叫TestThreadLocal.get()得到獨有的Id。接下來分析ThreadLocal的主要方法。

set方法

原始碼:

    public void set(T value) {
        // 獲取當前執行緒
        Thread t = Thread.currentThread();
        // 得到執行緒的threadLocals屬性,是ThreadLocalMap物件,其中k為這個ThreadLocal物件,v為value
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

從中可以看到ThreadLocalMap物件是實現功能的關鍵,整體思路和HashMap相似,具體程式碼就不細看了,有興趣可以自己點進去看,接下來只講述其中的關鍵點。ThreadLocalMap維護了一個Entry陣列,對ThreadLocal物件的HashCode進行處理後作為index將Entry物件新增到陣列中。接下來就是重中之重,Entry類:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

可以看到Entry類繼承了 WeakReference,他的弱引用指向了ThreadLocal物件,並且擁有屬性value。看下來可能有點暈了,給出一個圖方便理解

1-1-1

可以理解為每一個Thread都有一個ThreadLocalMap屬性,其中key為弱引用指向ThreadLocal,value為強引用指向傳入的物件。

為什麼要用弱引用作為key?

如果key為強引用,當我們現在將ThreadLocal的引用指向為null,但是每個執行緒中有自己獨立ThreadLocalMap,還會一直持有該物件,所以ThreadLocal物件不會被回收,會發生記憶體洩漏問題。如果key為弱引用,當我們現在將ThreadLocal的引用指向為null時,執行緒中獨立的ThreadLocalMap中的ThreadLocal物件會被回收。

還是有記憶體洩漏?

但是會發現就算是key被回收了,value也仍然被Entry中的value強引用指著不會被回收,依然會發生記憶體洩漏,所以在不用value的時候應該主動呼叫ThreadLocal物件的remove方法來移除。

remove方法

原始碼:

        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();   // 清理弱引用
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

expungeStaleEntry(i);Entry陣列的第i個entry物件的value置為null,然後將這個enrty物件置為null,最後進行rehash。

get方法

原始碼:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

在get方法中,通過getMap()獲得當前Thread物件的threadLocals屬性。在沒有呼叫set方法之前,threadLocals屬性為null,所以會呼叫setInitialValue()

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

可以看到,直接呼叫initialValue()方法得到value,然後設定並返回value,這就是前面為什麼重寫initialValue()方法。通過重寫initialValue()方法,給頂一個初始值,這樣在沒有呼叫set方法之前呼叫get方法就會從initialValue()中得到一個初始值。

相關文章