Java中的引用與ThreadLocal
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
。看下來可能有點暈了,給出一個圖方便理解
可以理解為每一個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()
中得到一個初始值。