前言
平時併發程式設計,除了維護修改共享變數的場景,有時我們也需要為每一個執行緒設定一個私有的變數,進行執行緒隔離,java提供的ThreadLocal可以幫助我們實現,而講到ThreadLocal則不得不講講java的四種引用,不同的引用型別在GC時表現是不一樣的,引用型別Reference有助於我們瞭解如何快速回收某些物件的記憶體或對例項的GC控制
- 四種引用型別在JVM的生命週期
- 引用佇列(ReferenceQueue)
- ThreadLocal的實現原理和使用
- FinalReference和finalize方法的實現原理
- Cheaner機制
關注公眾號,一起交流,微信搜一搜: 潛行前行
1 四種引用型別在JVM的生命週期
強引用(StrongReference)
- 建立一個物件並賦給一個引用變數,強引用有引用變數指向時,永遠也不會垃圾回收,JVM寧願丟擲OutOfMemory異常也不會回收該物件;強引用物件的建立,如
Integer index = new Integer(1);
String name = "csc";
- 如果中斷所有引用變數和強引用物件的聯絡(將引用變數賦值為null),JVM則會在合適的時間就會回收該物件
軟引用(SoftReference)
- 和強用引用不同點在於記憶體不足時,該型別引用物件會被垃圾處理器回收
- 使用軟引用能防止記憶體洩露,增強程式的健壯性。SoftReference的特點是它的一個例項儲存對一個Java物件的軟引用,該軟引用的存在不妨礙垃圾收集執行緒對該Java物件的回收
- SoftReference類所提供的get()方法返回Java物件的強引用。另外,一旦垃圾執行緒回收該物件之後,get()方法將返回null
String name = "csc";
//軟引用的建立
SoftReference<String> softRef = new SoftReference<String>(name);
System.out.println(softRef.get());
弱引用(WeakReference)
- 特點:無論記憶體是否充足,只要進行GC,都會被回收
String name = "csc";
//弱引用的建立
WeakReference<String> softRef = new WeakReference<String>(name);
System.out.println(softRef.get()); //輸出 csc
System.gc();
System.out.println(softRef.get()); //輸出 null
//弱引用Map
WeakHashMap<String, String> map = new WeakHashMap<String, String>();
虛引用(PhantomReference)
- 特點:如同虛設,和沒有引用沒什麼區別;虛引用和軟引用、弱引用不同,它並不決定物件的生命週期。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收
- 要注意的是,虛引用必須和引用佇列關聯使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收
public static void main(String[] args) {
String name = "csc";
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(name, queue);
//PhantomRefrence的get方法總是返回null,因此無法訪問對應的引用物件。
System.out.println(pr.get()); // null
System.gc();
System.out.println(queue.poll()); //獲取被垃圾回收的"xb"的引用ReferenceQueue
}
引用型別 | 被垃圾回收時間 | 場景 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 物件的一般狀態 | JVM停止執行時終止 |
軟引用 | 當記憶體不足時 | 物件快取 | 記憶體不足時終止 |
弱引用 | 正常垃圾回收時 | 物件快取 | 垃圾回收後終止 |
虛引用 | 正常垃圾回收時 | 跟蹤物件的垃圾回收 | 垃圾回收後終止 |
2 引用佇列(ReferenceQueue)
- 引用佇列可以配合軟引用、弱引用及虛引用使用;當引用的物件將要被JVM回收時,會將其加入到引用佇列中
ReferenceQueue<String> queue = new ReferenceQueue<String>();
WeakReference<String> pr = new WeakReference<String>("wxj", queue);
System.gc();
System.out.println(queue.poll().get()); // 獲取即將被回收的字串 wxj
3 ThreadLocal的原理和使用
ThreadLocal 的實現原理
- 每個執行緒都內建了一個ThreadLocalMap物件
public class Thread implements Runnable {
/* 當前執行緒對於的ThreadLocalMap例項,ThreadLocal<T>作為Key,
* T對應的物件作為value */
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap作為ThreadLocal的內部類,實現了類似HashMap的功能,它元素Entry繼承於WeakReference,key值是ThreadLocal,value是引用變數。也就是說jvm發生GC時value物件則會被回收
public class ThreadLocal<T> {
//ThreadLocal物件對應的hash值,使用一個靜態AtomicInteger實現
private final int threadLocalHashCode = nextHashCode();
//設定value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//獲取value
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//獲取當前執行緒的ThreadLocalMap,再使用物件ThreadLocal獲取對應的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
....
//類似HashMap的類
static class ThreadLocalMap {
//使用開放地址法解決hash衝突
//如果hash出的index已經有值,通過演算法在後面的若干位置尋找空位
private Entry[] table;
...
//Entry 是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal不保證共享變數在多執行緒的安全性
- 從ThreadLocal的實現原理可知,ThreadLocal只是為每個執行緒儲存一個副本變數,副本變數的修改不影響其他執行緒的變數值,因此ThreadLocal不能實現共享變數的安全性
ThreadLocal 使用場景
- 執行緒安全,包裹執行緒不安全的工具類,比如java.text.SimpleDateFormat類,當然jdk1.8已經給出了對應的執行緒安全的類java.time.format.DateTimeFormatter
- 執行緒隔離,比如資料庫連線管理、Session管理、mdc日誌追蹤等。
ThreadLocal記憶體洩露和WeakReference
- ThreadLocalMap.Entry是弱引用,弱引用物件是不管有沒有被引用都會被垃圾回收
- 發生記憶體洩漏一般是線上程池的執行緒,生命週期長,threadLocals引用會一直存在,當其存放的ThreadLocal被回收(弱引用生命週期短)後,它對應的Entity成了e.get()==null的例項。執行緒不死則Entity一直不會被回收,這就發生了記憶體洩漏
- 如果執行緒跨業務操作相同的ThreadLocal,還會造成變數安全問題
- 通常在使用完ThreadLocal最好呼叫它的remove();在ThreadLocal的get、set的時候,最好檢查當前Entity的key是否為null,如果是null就把Entity釋放掉,value則會被垃圾回收
4 finalize方法的實現原理FinalReference
final class Finalizer extends FinalReference<Object> {
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private static class FinalizerThread extends Thread {
....
public void run() {
...
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
//這裡會實現Object.finalize的呼叫
f.runFinalizer(jla);
....
}
static {
...
Thread finalizer = new FinalizerThread(tg);
... //執行Object.finalize的守護執行緒
finalizer.setDaemon(true);
finalizer.start();
}
- cpu資源比較稀缺的情況下FinalizerThread執行緒有可能因為優先順序比較低而延遲執行finalizer物件的finalize方法
- 因為finalizer物件的finalize方法遲遲沒有執行,有可能會導致大部分finalizer物件進入到old分代,此時容易引發old分代的gc,甚至fullgc,gc暫停時間明顯變長
5 Cheaner機制
- 上一篇文章有介紹到jdk1.8的Cleaner框架篇:ByteBuffer和netty.ByteBuf詳解