Java對synchronized鎖的實現與優化以及四大引用

早期的synchronized鎖

在Java 1.5之前,多線程併發中,synchronized一直都是一個元老級關鍵字,而且給人的一貫印象就是一個比較重的鎖。爲此,在Java 1.6之後,這個關鍵字被做了很多的優化,從而讓以往的“重量級鎖”變得不再那麼重。

synchronized主要有兩種使用方法,一種是修飾代碼塊,一種是修飾方法。這兩種用法底層究竟是怎麼實現的呢?在1.6之前是怎麼實現的呢?在java語言中存在兩種內建的synchronized語法:

  1. synchronized語句;
  2. synchronized方法;

對於synchronized語句當Java源代碼被javac編譯成字節碼的時候,會在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit的字節碼指令。

而synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在JVM字節碼層面並沒有任何特別的指令來標記被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標誌位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象(synchronized修飾靜態方法時,因爲靜態方法屬於類)表示做爲鎖對象

那麼monitorenter和monitorexit以及access_flags底層又是通過什麼技術來實現的原子操作呢?

互斥鎖(Mutex Lock)

簡單來說在JVM中monitorenter和monitorexit字節碼依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起(線程狀態變爲阻塞)並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的

Mutex Lock互斥鎖主要用於實現內核中的互斥訪問功能。Mutex Lock內核互斥鎖是在原子API之上實現的,但這對於內核用戶是不可見的。對它的訪問必須遵循一些規則:同一時間只能有一個任務持有互斥鎖,而且只有這個任務可以對互斥鎖進行解鎖。互斥鎖不能進行遞歸鎖定或解鎖。一個互斥鎖對象必須通過其API初始化,而不能使用memset或複製初始化。一個任務在持有互斥鎖的時候是不能結束的。互斥鎖所使用的內存區域是不能被釋放的。使用中的互斥鎖是不能被重新初始化的。並且互斥鎖不能用於中斷上下文。但是互斥鎖比當前的內核信號量選項更快,並且更加緊湊,因此如果它們滿足您的需求,那麼它們將是您明智的選擇。

在硬件層面,CPU提供了原子操作、關中斷、鎖內存總線的機制;OS基於這幾個CPU硬件機制,就能夠實現鎖;再基於鎖,就能夠實現各種各樣的同步機制(信號量、消息、Barrier等)。所以要想理解OS的各種同步手段,首先需要理解cpu層面的鎖,這是最原點的機制,所有的OS上層同步手段都基於此。

Mutex Lock從用戶態切換到內核態

Linux操作系統的體系架構分爲用戶態和內核態(或者用戶空間和內核)。內核從本質上看是一種軟件——控制計算機的硬件資源,並提供上層應用程序運行的環境。用戶態即上層應用程序的活動空間,應用程序的執行必須依託於內核提供的資源,包括CPU資源、存儲資源、I/O資源等。爲了使上層應用能夠訪問到這些資源,內核必須爲上層應用提供訪問的接口:即系統調用。

內核態:CPU可以訪問內存所有數據,包括外圍設備,例如硬盤、 網卡。CPU也可以將自己從一個程序切換到另一個程序。

用戶態:只能受限的訪問內存,且不允許訪問外圍設備。佔用CPU的能力被剝奪,CPU資源可以被其他程序獲取。

因爲操作系統的資源是有限的,如果訪問資源的操作過多,必然會消耗過多的資源,而且如果不對這些操作加以區分,很可能造成資源訪問的衝突。所以爲了減少有限資源的訪問和使用衝突,Unix/Linux的設計哲學之一就是:對不同的操作賦予不同的執行等級,就是所謂特權的概念。簡單說就是有多大能力做多大的事,與系統相關的一些特別關鍵的操作必須由最高特權的程序來完成。Intel的X86架構的CPU提供了0到3四個特權級,數字越小,特權越高,Linux操作系統中主要採用了0和3兩個特權級,分別對應的就是內核態和用戶態。運行於用戶態的進程可以執行的操作和訪問的資源都會受到極大的限制,而運行在內核態的進程則可以執行任何操作並且在資源的使用上沒有限制。很多程序開始時運行於用戶態,但在執行的過程中,一些操作需要在內核權限下才能執行,這就涉及到一個從用戶態切換到內核態的過程。比如C函數庫中的內存分配函數malloc(),它具體是使用sbrk()系統調用來分配內存,當malloc調用sbrk()的時候就涉及一次從用戶態到內核態的切換,類似的函數還有printf(),調用的是wirte()系統調用來輸出字符串,等等

所有用戶程序都是運行在用戶態的, 但是有時候程序確實需要做一些內核態的事情,例如從硬盤讀取數據,或者從鍵盤獲取輸入等。而唯一可以做這些事情的就是操作系統, 所以此時程序就需要先請求操作系統以程序的名義來執行這些操作。

這時需要一個這樣的機制:用戶態程序切換到內核態,但是不能控制在內核態中執行的指令。這種機制叫系統調用, 在CPU中的實現稱之爲陷阱指令(Trap Instruction)。

用戶態程序切換到內核態的流程如下:

  • 用戶態程序將一些數據值放在寄存器中或者使用參數創建一個堆棧(stack frame),以此表明需要操作系統提供的服務
  • 用戶態程序執行系統調用,即陷阱指令
  • CPU切換到內核態,並跳到位於內存指定位置的指令,這些指令是操作系統的一部分,他們具有內存保護,不可被用戶態程序訪問
  • 這些指令稱之爲陷阱(trap)或者系統調用處理器(system call handler), 他們會讀取程序放入內存的數據參數, 並執行程序請求的服務
  • 系統調用完成後, 操作系統會重置CPU爲用戶態並返回系統調用的結果

JDK1.6對synchronized鎖的優化

簡單來說在JVM中monitorenter和monitorexit字節碼依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的。然而在現實中的大部分情況下,同步方法是大多數是運行在單線程環境(無鎖競爭),如果每次都調用Mutex Lock那麼將嚴重的影響程序的性能。不過在JDK1.6中對鎖的實現引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷。

鎖粗化(Lock Coarsening): 也就是減少不必要的緊連在一起的lock/unlock操作,將多個連續的鎖擴展成一個範圍更大的鎖。

鎖消除(Lock Elimination): 通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數據的鎖保護,通過逃逸分析也可以在線程本地Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。

輕量級鎖(Lightweight Locking): 這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的情況下完全可以避免調用操作系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的線程將調用操作系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒

偏向鎖(Biased Locking): 比輕量級鎖更輕,是爲了在無鎖競爭的情況下避免在鎖獲取過程中執行不必要的CAS原子指令,因爲CAS原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲。

適應性自旋(Adaptive Spinning): 當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的操作系統重量級鎖(mutex lock)前會進入忙等待(Spinning)然後再次嘗試,當嘗試一定的次數後如果仍然沒有成功則調用與該monitor關聯的互斥鎖進入到阻塞狀態。

自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,“自旋”一詞就是因此而得名。

也就是說自旋鎖就是一直在那裏刷新,看看鎖有沒有被釋放。而不是像傳統的那種等待正在調用的線程釋放鎖後,然後通知這些等待的鎖然後喚醒。

下面具體闡述JDK1.6是怎麼實現偏向鎖、輕級鎖的以及鎖怎樣升級爲重量級互斥鎖的。

不過,在具體闡述之前,要先了解一下Java對象結構。

1、Java對象的創建與內存佈局

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、 解析和初始化過。 如果沒有,那必須先執行相應的類加載過程。

在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。 對象所需內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。因此,在使用Serial、ParNew等帶Compact(整理)過程的收集器時,系統採用的分配算法是指針碰撞,因爲上述垃圾收集算法運行後,空閒區域是連續的,內存碎片少。而使用CMS這種基於Mark-Sweep算法的收集器時,通常採用空閒列表

  • 指針碰撞:假設Java堆中的內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,分配內存就是把這個指針向空閒的內存那邊挪動一段與對象大小相等的距離
  • 空閒列表:假設Java堆中的內存是不規整的,虛擬機就必須維護一個表,用來記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間分對象,並更新表上的記錄

對象創建在虛擬機中是非常頻繁的行爲,即使是僅僅修改一個指針所指向的位置,在併發情況下也不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。這個時候主要有兩個解決方案:一種是對分配內存空間的動作進行同步處理——實際上虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTlAB參數來設定。

Java對象由三部分構成:對象頭、實例數據、對齊補充。

對象頭

第一部分是與對象在運行時狀態相關的信息,長度通過與操作系統的位數保持一致。包括對象的哈希值、GC分代年齡、鎖狀態以及偏向線程的ID等。由於對象頭信息是與對象所定義的信息無關的數據,所以使用了非固定的數據結構,以便存儲更多的信息,實現空間複用。因此對象在不同的狀態下對象頭的存儲信息有所差別。
在這裏插入圖片描述
另一部分是類型指針,即指向該對象所屬類元數據的指針,虛擬機通常通過這個指針來確定該對象所屬的類型(但並不是唯一方式)。

另外,如果對象是一個數組,在對象頭中還應該有一塊記錄數組長度的數據,因爲JVM可以通過對象的元數據確定對象的大小,但不能通過元數據確定數組的長度。

實例數據

實例數據存儲的是真正的有效數據,即各個字段的值。無論是子類中定義的,還是從父類繼承下來的都需要記錄。這部分數據的存儲順序受到虛擬機的分配策略以及字段在類中的定義順序的影響。

對齊填充

這部分數據不是必然存在的,因爲對象的大小總是8字節的整數倍,該數據僅用於補齊實例數據部分不足整數倍的部分,充當佔位符的作用。

2、Java對象頭存儲鎖數據

每個Java對象都可以用做一個實現同步的鎖,這些鎖被稱爲內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock)。要實現這個目標,則每個Java對象都應該與某種類型的鎖數據關聯。這就意味着,我們需要一個存儲鎖數據的地方,並且每一個對象都應該有這麼個地方。在Java中,這個地方就是對象頭

其實Java的對象頭和對象的關係很像Http請求的http header和http body的關係。對象頭中存儲了該對象的Metadata, 除了該對象的鎖信息,還包括指向該對象對應的類的指針、對象的hashcode、 GC分代年齡等,在對象頭這個寸土寸金的地方,根據鎖狀態的不同,有些內存是大家公用的,在不同的鎖狀態下,存儲不同的信息

synchronized是一種悲觀鎖,鎖是存在對象頭中的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit。

在這裏插入圖片描述
Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位。在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲以下4種數據:

在這裏插入圖片描述
synchronized代碼塊是由一對兒monitorenter/monitorexit 指令實現的,Monitor對象是同步的基本實現單元。在synchronized鎖中,存儲在對象頭的Mark Word中的鎖信息是一個指針,它指向一個Monitor對象(也稱爲管程或監視器鎖)的起始地址。這樣,我們就通過對象頭,將每一個對象與一個Monitor關聯了起來,它們的關係如下圖所示:
在這裏插入圖片描述
圖片的最左邊是線程的調用棧,它引用了堆中的一個對象,該對象的對象頭部分記錄了該對象所使用的監視器鎖,該監視器鎖指向了一個Monitor對象。

那麼這個Monitor對象是什麼呢? 在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor實現的,其數據結構如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
 }

上面這些字段中,我們只需要重點關注三個字段:

  • _owner:當前擁有該 ObjectMonitor 的線程
  • _EntryList:當前等待鎖的集合
  • _WaitSet:調用了Object.wait()方法而進入等待狀態的線程的集合

在Java中,每一個等待鎖的線程都會被封裝成ObjectWaiter對象(ObjectWaiter類由JVM定義,ObjectWaiter對象裏存放thread),當多個線程同時訪問一段同步代碼時,首先會被扔進 _EntryList 集合中,如果其中的某個線程獲得了monitor對象,它將成爲 _owner,如果在它成爲 _owner之後又調用了wait()方法,則他將釋放獲得的Monitor對象,進入 _WaitSet集合中等待被喚醒

在這裏插入圖片描述

另外,因爲每一個對象都可以作爲synchronized的鎖,所以每一個對象都必須支持wait()、notify()、notifyAll()方法,使得線程能夠在一個Monitor對象上wait,直到它被notify。這也就解釋了這三個方法爲什麼定義在了Object類中——這樣,所有的類都將持有這三個方法。Object類的wait()和notify()都被標記爲native方法,其具體實現都在JVM的synchronizer.cpp裏。

所以說每一個Java對象都可以作爲鎖,其實是指將每一個Java對象所關聯的ObjectMonitor作爲鎖,更進一步是指,大家都想成爲某一個Java對象所關聯的ObjectMonitor對象的_owner,所以你可以把這個_owner看做是鐵王座,所有等待在這個監視器鎖上的線程都想坐上這個鐵王座,誰擁有了它,誰就有進入由它鎖住的同步代碼塊的權利

3、Monitor鎖在JDK1.6中多種實現

根據前面的分析,我們知道在Java 6之前,Monitor的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。現代的(Oracle)JDK 中,JVM對此進行了大刀闊斧地改進,提供了三種不同的Monitor實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

所謂鎖的升級、降級,就是JVM優化synchronized運行的機制,當JVM檢測到不同的競爭狀況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級

由於synchronized是JVM內部的Intrinsic Lock,所以偏斜鎖、輕量級鎖、重量級鎖的代碼實現,並不在核心類庫部分,而是在JVM的代碼中。

偏向鎖

HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。當沒有競爭出現時,默認會使用偏向鎖。當一個線程訪問同步塊並獲取鎖時,JVM會利用CAS操作(compareAndSwap),在對象頭上的Mark Word部分設置線程ID,以表示這個對象偏向於當前線程,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,所以並不涉及真正的互斥鎖。只需要測試一下對象頭的MarkWord裏是否存儲着當前線程ID,成功則表示線程已經獲得了鎖。

這樣做的假設是基於在很多應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏向鎖可以降低無競爭開銷。偏向鎖是一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程就會釋放鎖,即撤銷偏向鎖。JVM首先暫停擁有鎖的線程,然後檢查持有偏向鎖的線程是否依然存活,若不再存活就將對象頭設置成無鎖狀態;如果線程仍然存活,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼偏向其他線程,要麼恢復到無鎖,或者標記對象不適合作爲偏向鎖,最後喚醒暫停的線程。

偏向鎖獲取與撤銷流程如下圖所示:
在這裏插入圖片描述

輕量級鎖

如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM就需要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現(已經出現多個線程競爭鎖,偏向鎖的假設不再成立)。輕量級鎖依賴CAS操作Mark Word來試圖獲取鎖,如果CAS操作成功,就使用普通的輕量級鎖;否則,進一步升級爲重量級鎖。

輕量級鎖加鎖:線程在執行同步塊之前,JVM會先在當前線程棧楨中創建用於存儲鎖記錄的空間。並將對象頭中的Mark Word複製到鎖記錄中(官方稱Displaced Mark Word),然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,則當前線程獲取鎖,失敗則代表其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖

輕量級鎖解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功則表示無競爭發生,如果失敗代表鎖存在競爭,鎖進一步膨脹成重量級鎖。

因爲自旋會消耗CPU,爲了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖(依賴操作系統提供的互斥量),就不會再恢復成輕量級鎖的狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭

多個線程爭奪鎖導致鎖導致輕量級鎖膨脹的流程圖如下:
在這裏插入圖片描述

三種類型鎖的優缺點對比如下:
在這裏插入圖片描述

Java對引用的定義

無論是通用引用計數算法判斷對象的引用數據,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。在JDK1.2之前,Java中的引用定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能爲力。我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存中,如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。

在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次逐漸減弱。
強引用就是指在程序代碼中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會
回收掉被引用的對象。

Java中根據其生命週期的長短,將引用分爲4類。

強引用

特點:我們平常典型編碼Object obj = new Object()中的obj就是強引用。通過關鍵字new創建的對象所關聯的引用就是強引用。 當JVM內存空間不足,JVM寧願拋出OutOfMemoryError運行時錯誤(OOM),使程序異常終止,也不會靠隨意回收具有強引用的“存活”對象來解決內存不足的問題。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,具體回收時機還是要看垃圾收集策略。

軟引用

特點:軟引用通過SoftReference類實現。 軟引用的生命週期比強引用短一些。只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象:即JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。後續,我們可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的對象被回收。如果隊列爲空,將返回一個null,否則該方法返回隊列中前面的一個Reference對象。

應用場景:軟引用通常用來實現內存敏感的緩存。如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

弱引用

弱引用通過WeakReference類實現。 弱引用的生命週期比軟引用短。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。由於垃圾回收器是一個優先級很低的線程,因此不一定會很快回收弱引用的對象。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

應用場景:弱應用同樣可用於內存敏感的緩存。

虛引用

特點:虛引用也叫幻象引用,通過PhantomReference類來實現。無法通過虛引用訪問對象的任何屬性或函數。幻象引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。

ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取一些程序行動。

應用場景:可用來跟蹤對象被垃圾回收器回收的活動,當一個虛引用關聯的對象被垃圾收集器回收之前會收到一條系統通知。

關於強引用之外的另外三大引用,可以用以下代碼來測試

public class TestReference {

    private static List<Object> list = new ArrayList<>();

    public static int M = 1024 * 1024;

    public static void main(String[] args) {
        testSoftReference();
        System.out.println("------------");
        list.clear();
        testWeakReference();
        System.out.println("------------");
        testPhantomReference();
    }

    private static void testSoftReference() {
        Runtime runtime = Runtime.getRuntime();
        String value = "我是軟引用";
        SoftReference<String> sfRefer = new SoftReference<>(value);
        //可以獲得引用對象值
        System.out.println(sfRefer.get());
        for (int i = 0; i < 10; i++) {
            byte[] buff = new byte[M];
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }
        System.out.println(runtime.freeMemory() / M + "M(free) / " + runtime.maxMemory() / M + "M(max)");
        //主動觸發垃圾回收
        System.gc();
		//垃圾回收後,再查看數據
        for (int i = 0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
        //再申請4MB的空間,觸發垃圾回收
        SoftReference<Object> softReference = new SoftReference<Object>(new byte[4 * M]);
        System.out.println("softReference.get() : " + softReference.get());
        for (int i = 0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
    }

    private static void testWeakReference() {
        Runtime runtime = Runtime.getRuntime();
        String value = "我是弱引用";
        WeakReference<String> wkRefer = new WeakReference<>(value);
        //可以獲得引用對象值
        System.out.println(wkRefer.get());
        //創建10MB數組
        for (int i = 0; i < 10; i++) {
            byte[] buff = new byte[M];
            WeakReference<byte[]> sr = new WeakReference<>(buff);
            list.add(sr);
        }
        System.out.println(runtime.freeMemory() / M + "M(free) / " + runtime.maxMemory() / M + "M(max)");
        //主動觸發垃圾回收
        System.gc();
        //垃圾回收後,再查看數據
        for(int i = 0; i < list.size(); i++){
            Object obj = ((WeakReference) list.get(i)).get();
            System.out.println(obj);
        }

    }

    private static void testPhantomReference() {
        // 創建一個字符串對象
        String str = new String("Java的4大引用");
        // 創建一個引用隊列
        ReferenceQueue rq = new ReferenceQueue();
        // 創建一個虛引用,讓此虛引用引用到"瘋狂Java講義"字符串
        PhantomReference pr = new PhantomReference (str , rq);
        // 切斷str引用和"Java的4大引用"字符串之間的引用
        str = null;
        // 取出虛引用所引用的對象,並不能通過虛引用獲取被引用的對象,所以此處輸出null
        System.out.println(pr.get());
        // 強制垃圾回收
        System.gc();
        System.runFinalization();
        // 垃圾回收之後,虛引用將被放入引用隊列中
        // 取出引用隊列中最先進入隊列中的引用與pr進行比較
        System.out.println(rq.poll() == pr);
    }
}

在筆記本上運行效果如下,可以看到默認的JVM可用內存很大,垃圾回收時在內存空間足夠用的情況下,軟引用對象佔用的空間不會被回收。

我是軟引用
231M(free) / 3641M(max)
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
softReference.get() : [B@4dc63996
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
------------
我是弱引用
219M(free) / 3641M(max)
null
null
null
null
null
null
null
null
null
null
------------
null
true

Process finished with exit code 0

現在設置-Xms2M -Xmx15M,然後重新運行,效果如下:

我是軟引用
1M(free) / 14M(max)
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
softReference.get() : [B@4dc63996
null
null
null
null
null
null
null
null
null
null
------------
我是弱引用
2M(free) / 14M(max)
null
null
null
null
null
null
null
null
null
null
------------
null
true

Process finished with exit code 0

軟引用的實際應用

如果一個對象只具有軟引用,那就類似於可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

比如在圖片加載框架中,通過軟引用來實現內存緩存。

//實現圖片異步加載的類
public class AsyncImageLoader {
    //以Url爲鍵,SoftReference類型爲值,建立緩存HashMap鍵值對。
    private Map<String, SoftReference<Drawable>> mImageCache = new HashMap<String, SoftReference<Drawable>>();
     
    //實現圖片異步加載
    public Drawable loadDrawable(final String imageUrl, final ImageCallback callback) {
        //查詢緩存,查看當前需要下載的圖片是否在緩存中
        if(mImageCache.containsKey(imageUrl)) {
            SoftReference<Drawable> softReference = mImageCache.get(imageUrl);
            if (softReference.get() != null) {
                return softReference.get();
            }
        }
         
        final Handler handler = new Handler() {
            @Override
            public void dispatchMessage(Message msg) {
                //回調ImageCallbackImpl中的imageLoad方法,在主線(UI線程)中執行。
                callback.imageLoad((Drawable)msg.obj);
            }
        };
         
        /*若緩存中沒有,新開闢一個線程,用於進行從網絡上下載圖片,
         * 然後將獲取到的Drawable發送到Handler中處理,通過回調實現在UI線程中顯示獲取的圖片
         */
        new Thread() {      
            public void run() {
                Drawable drawable = loadImageFromUrl(imageUrl);
                //將得到的圖片存放到緩存中
                mImageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
                Message message = handler.obtainMessage(0, drawable);
                handler.sendMessage(message);
            };
        }.start();
         
        //若緩存中不存在,將從網上下載顯示完成後,此處返回null;
        return null;
    }
     
    //定義一個回調接口
    public interface ImageCallback {
        void imageLoad(Drawable drawable);
    }
     
    //通過Url從網上獲取圖片Drawable對象;
    protected Drawable loadImageFromUrl(String imageUrl) {
        try {
            return Drawable.createFromStream(new URL(imageUrl).openStream(),"debug");
        } catch (Exception e) {
            // TODO: handle exception
            throw new RuntimeException(e);
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章