java四種引用方式與ThreadLocal解析

java四種引用類型

從jdk1.2以來,java把對象的引用定義為四種級別,從而使程序可以更加靈活地控制對象的生命周期。四種引用類型按照由強到弱的順序分別為:強引用、軟引用、弱引用、虛引用。

強引用

public static void main(String[] args) throws IOException {
    M m = new M();
    m = null;
    System.gc();
    System.in.read();
}

m就是持有堆內(nèi)M對象的一個強引用,也是我們最常見的一種引用形式,屬于不可回收資源,垃圾回收期不會主動回收它。當內(nèi)存空間不足的時候,jvm會拋出OutOfMemoryError錯誤,也不會主動釋放強引用對象。

如果想釋放強引用對象,可以顯示的把引用復制為null,這樣jvm就會在合適的時間釋放掉強引用對象。

軟引用

SoftReference<byte[]> m = new SoftReference(new byte[1024 * 1024 * 10]);
System.out.println(m.get());
System.gc();
try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    e.printStackTrace();
}
byte[] b = new byte[1024 * 1024 * 15];
System.out.println(m.get() );

如果內(nèi)存空間足夠,垃圾回收器不會回收軟引用對象,如果內(nèi)存空間不足,垃圾回收器一定會回收軟引用的對象。

弱引用

public static void main(String[] args) {
    WeakReference<M> m = new WeakReference<>(new M());
    System.out.println(m.get());  // 返回對象
    System.gc();
    System.out.println(m.get());  // 返回null
}

弱引用有更短的生命周期,垃圾回收器只要掃描到弱引用對象,就會清除弱引用對象。

虛引用

private static final List<Object> LIST = new LinkedList<>();
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

public static void main(String[] args) {
    PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
    System.out.println(phantomReference.get());

    new Thread(()->{
        for (int i = 0; i < 30; i++) {
            LIST.add(new byte[1024 * 1024]);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
            System.out.println(phantomReference.get());
        }

    }).start();

    new Thread(()->{
        while (true) {
            Reference<? extends M> poll = QUEUE.poll();
            if (poll != null) {
                System.out.println("虛引用對象被jvm回收了"+poll);
            }
        }
    }).start();

    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

虛引用不影響對象的生命周期。虛引用需要和引用隊列相關(guān),當垃圾回收器準備回收一個對象,發(fā)現(xiàn)它還有虛引用,就會把虛引用加入到與之關(guān)聯(lián)的引用隊列中。

ThreadLocal與弱引用

ThreadLocal是什么

ThreadLocal是java.lang包中實現(xiàn)相同線程數(shù)據(jù)共享、不同線程數(shù)據(jù)隔離的工具。

static final ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+tl.get());
    }).start();

    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tl.set("hello");
    }).start();
    
}

ThreadLocal和鎖之間的關(guān)系:鎖解決的是線程之間數(shù)據(jù)共享的問題,ThreadLocal出現(xiàn)的場景是線程之間沒有數(shù)據(jù)共享,只涉及到同一個線程不同位置使用數(shù)據(jù)的問題。

ThreadLocal實現(xiàn)原理

對象實例(對應value)和ThreadLocal對象實例(對應key)是由線程Thread來維護的。對象實例和ThreadLocal實例的映射關(guān)系存放在Thread對象中的一個map屬性中,這個map的類型為ThreadLocalMap。

由于ThreadLocalMap沒有對外暴露方法,想要修改或者獲取map中的值只能通過ThreadLocal的api來完成。如果Threadlocal對象銷毀了,那么就沒有辦法獲取到map中的值了。

// ThreadLocal的set方法
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 可以看到map是Thread對象的一個屬性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

// 創(chuàng)建map,并賦值給Thread對象的一個屬性
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

所以涉及的值存儲的地方都是由ThreadLocalMap來完成的,ThreadLocalMap維護了ThreadLocal對象和實例對象的一個映射關(guān)系。

// 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;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap中使用Entry[]數(shù)組來維護鍵值對的映射關(guān)系,其中key為ThreadLocal對象,value為實例對象value。并且這里的key使用了弱引用,如果對應的ThreadLocal對象的強引用不存在了,這里面的ThreadLocal對象也會被清理掉。

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

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

這樣設(shè)計的目的主要是:如果ThreadLocal對象已經(jīng)不在被強引用了,那么已經(jīng)沒有辦法再訪問到map中的值了,因為通過Thread對象的屬性訪問map,然后map再訪問value的路徑走不通(map的方法不對外暴露)。這個時候可以認為key已經(jīng)沒有用了。

那么為什么不把value也設(shè)置為弱引用呢?因為不清楚這個value除了map的引用是否還存在其他的引用。如果是弱引用,當其他地方value的強引用被清理掉之后,gc時就把value(只有弱引用)直接干掉了。Threadlocal對象作為的key還存在,而value卻不存在了,顯然是不合適的。

ThreadLocal與內(nèi)存泄露

當把ThreadLocal對象的強引用設(shè)置為null之后,就只剩下TheadLocalMap的key為弱引用類型,那么在下次gc的時候會回收掉ThreadLocal對象占用的空間,key就變成null了,而value還存在沒有被回收。而且由于map是Thread對象的一個屬性,只要Thread對象不銷毀,那么value的強引用就一直存在,這樣就導致了內(nèi)存泄露。如果Thread對象過一段時間就銷毀,那么影響不會特別大,如果是在線程池場景下,Thread對象一直不銷毀,就會導致value一直存在。

所以當線程中的某個ThreadLocal對象使用完了,需要立即調(diào)用remove方法,刪除Entry對象。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容