Java中的引用類型和使用場景
- 2021 年 10 月 7 日
- 筆記
- JAVA, threadLocal
作者:Grey
原文地址:Java中的引用類型和使用場景
Java中的引用類型有哪幾種?
Java中的引用類型分成強引用, 軟引用, 弱引用, 虛引用。
強引用
沒有引用指向這個對象,垃圾回收會回收
package git.snippets.juc;
import java.io.IOException;
public class NormalRef {
public static void main(String[] args) throws IOException {
M m = new M();
m = null;
System.gc();
System.in.read();
}
static class M {
M() {}
@Override
protected void finalize() throws Throwable {
System.out.println("finalized");
}
}
}
軟引用
當有一個對象被一個軟引用所指向的時候,只有系統記憶體不夠用的時候,才會被回收,可以用做快取(比如快取大圖片)
示例如下程式碼:註:執行以下方法的時候,需要把VM options設置為-Xms20M -Xmx20M
。
package git.snippets.juc;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;
/**
* heap將裝不下,這時候系統會垃圾回收,先回收一次,如果不夠,會把軟引用幹掉
* 軟引用,適合做快取
* 示例需要把Vm options設置為:-Xms20M -Xmx20M
*/
public class SoftRef {
public static void main(String[] args) throws IOException {
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024 * 10]);
System.out.println(reference.get());
System.gc();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(reference.get());
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println(reference.get());
System.in.read();
}
}
上述程式碼在第一次執行System.out.println(reference.get())
時候,由於堆的最大最小值都是20M
,而我們分配的byte
數組是10M
,沒有超過最大堆記憶體,所以執行垃圾回收,軟引用不被回收,後續又調用了byte[] bytes = new byte[1024 * 1024 * 10];
再次分配了10M
記憶體,此時堆記憶體已經超過設置的最大值,會進行回收,所以最後一步的System.out.println(reference.get());
無法get
到數據。
弱引用
只要垃圾回收,就會回收。如果有一個強引用指向弱引用中的這個對象,如果這個強引用消失,這個對象就應該被回收。一般用在容器裡面。
程式碼示例如下:
package git.snippets.juc;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* 弱引用遭到gc就會回收
* ThreadLocal應用,快取應用,WeakHashMap
*/
public class WeakRef {
public static void main(String[] args) {
WeakReference<T> reference = new WeakReference<>(new T());
System.out.println(reference.get());
System.gc();
System.out.println(reference.get());
}
static class T {
T() {}
@Override
protected void finalize() {
System.out.println("finalized");
}
}
}
如果執行了一次GC
,reference.get()
獲取到的值即為空。
弱引用的使用場景
弱引用的一個典型應用場景就是ThreadLocal
,以下是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);
}
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();
}
ThreadLocalMap
是當前執行緒的一個成員變數,所以,其他執行緒無法讀取當前執行緒設置的ThreadLocal
值。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal
的主要應用場景
場景一:每個執行緒需要一個獨享的對象:假設有100個執行緒都需要用到SimpleDateFormat
類來處理日期格式,如果共用一個SimpleDateFormat
,就會出現執行緒安全問題,導致數據出錯,如果加鎖,就會降低性能,此時使用ThreadLocal
,給每個執行緒保存一份自己的本地SimpleDateFormat
,就可以同時保證執行緒安全和性能需求。
場景二:每個執行緒內部保存全局變數,避免傳參麻煩:假設一個執行緒的作用是拿到前端用戶資訊,逐層執行Service1
,Service2
,Service3
,Service4
層的業務邏輯,其中每個業務層都會用到用戶資訊,此時一個解決辦法就是將User
資訊對象作為參數層層傳遞,但是這樣會導致程式碼冗餘且不利於維護。此時可以將User
資訊對象放入當前執行緒的Threadlocal
中,就變成了全局變數,在每一層業務層中,需要使用的時候直接從Threadlocal
中獲取即可。
場景三:Spring
的聲明式事務,資料庫連接寫在配置文件,多個方法可以支援一個完整的事務,保證多個方法是用的同一個資料庫連接(其實就是放在ThreadLocal
裡面)
了解了ThreadLocal
簡要介紹以後,我們可以深入理解一下ThreadLocal
的一個內部原理,前面提到,ThreadLocal
的set
方法實際上是往當前執行緒的一個threadLocals
表中插入一條記錄,而這個表中的記錄都存在一個Entry
對象中,這個對象有一個key和一個value,key
就是當前執行緒的ThreadLocal
對象。
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
, 且構造函數調用了super(k)
, 所以Entry
中的key
是通過一個弱引用指向的ThreadLocal
,所以,我們在主方法中調用
ThreadLocal<Object> tl = new ThreadLocal<>();
tl
是通過強引用指向這個ThreadLocal
對象。
當前執行緒的threadLocalMap
中的key
是通過弱引用指向ThreadLocal
對象,這樣就可以保證,在tl
指向空以後,這個ThreadLocal
會被回收,否則,如果threadLocalMap
中的key
是強引用指向ThreadLocal
對象話,這個ThreadLocal
對象永遠不會被回收。就會導致記憶體泄漏。
但是,即便key
用弱引用指向ThreadLocal
對象,key
值被回收後,Entry
中的value
值就無法被訪問到了,且value
是通過強引用關聯,所以,也會導致記憶體泄漏,所以,每次在ThreadLocal
中的對象不用了,記得要調用remove
方法,把對應的value
也給清掉。
虛引用
用於管理堆外記憶體回收
虛引用關聯了一個對象,以及一個隊列,只要垃圾回收,虛引用就被回收,一旦虛引用被回收,虛引用會被裝到這個隊列,並會收到一個通知(如果有值入隊列,會得到一個通知)所以,如果想知道虛引用何時被回收,就只需要不斷監控這個隊列是否有元素加入進來了。
虛引用裡面關聯的對象用get方法是無法獲取的。
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
// 配置 -Xms20M -Xmx20M
public class PhantomRef {
private static final List<Object> LIST = new LinkedList<>();
private static final ReferenceQueue<P> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<P> phantomReference = new PhantomReference<>(new P(), QUEUE);
new Thread(() -> {
while (true) {
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 P> poll = QUEUE.poll();
if (poll != null) {
System.out.println("--- 虛引用對象被jvm回收了 ---- " + poll);
}
}
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class P {
@Override
protected void finalize() throws Throwable {
System.out.println("finalized");
}
}
}
虛引用的應用場景
JDK的NIO
包中有一個DirectByteBuffer
, 這個buffer
指向的是堆外記憶體,所以當這個buffer
設置為空的時候,Java的垃圾回收無法回收,所以,可以用虛引用來管理這個buffer
,當我們檢測到這個虛引用被垃圾回收器回收的時候,可以做出相應的處理,去回收堆外記憶體。