前言:在慕課網上學習劍指Java面試-Offer直通車時所做的筆記,供本人複習之用,比較難,我也沒大懂,只記錄大概意思以後有接觸了再改,想要詳細解說的不建議看這篇博客.
目錄
第五章 Synchronized和ReentrantLock的區別
第一章 對象在內存中的佈局
HotSpot 對象在內存中的佈局分爲三塊區域,對象頭,實例數據,對齊填充,我們在這裏主要講解對象頭.
synchronized使用的鎖對象是存儲在java對象頭裏的,其主要結構是由Mark Word和Class Metadata Address組成,Class Metadata Address是指向類元數據的指針,虛擬機通過這個指針確認其是哪個對象的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵.
Mark Word:
第二章 Monitor
上面介紹了java對象頭,下面我們介紹Monitor.
Monitor:每個對象天生自帶了一把看不見的鎖,叫做內部鎖或者Monitor鎖.
Monitor也稱爲管程或者監視器鎖,我們可以把它理解爲一個同步工具,也可以描述爲一種同步機制,通常它被描述爲一個對象.
這裏我們拿重量級鎖進行分析,鎖的標識位爲10,指針指向的是monitor對象的起始地址,每個對象都存在着一個monitor與之關聯,monitor存在於對象的對象頭中,對象與monitor之間有多存在多種實現方式,如monitor可以與對象一起存在銷燬,或當線程試圖獲取對象鎖時,自動生成.當monitor被某個線程持有後,它便處於鎖定狀態.
在java虛擬機HotSpot中monitor由ObjectMonitor來實現,位於HotSpot虛擬機源碼objectMonitor裏,通過C++來實現.
ObjectMonitor中有兩個隊列,一個是WaitSet一個是EntryList,可以與等待池與鎖池聯繫起來,他們就是用來保存ObjectWaiter的對象列表,每個對象鎖的線程都會被封裝成ObjectWaiter來保存到裏面,owner指向持有ObjectMonitor的線程.
當多個線程訪問同一段同步代碼的時候,首先會進入到EntryList集合中,當線程獲取到對象的Monitor之後,就進入到Object區域,並把Monitor中的Owner變量設置爲當前線程,同時Monitor中的count就會加1,如果線程調用wait方法將會釋放當前持有的Monitor,owner會被恢復成null,count也會被減1,同時該線程ObjectWaiter實例就會進入到waitSet集合等待被喚醒,若當前線程執行完畢,它也將釋放monitor鎖,並復位對應變量的值,以便其它線程進入獲取monitor鎖.
Monitor鎖的競爭,獲取與釋放.
2.1 Monitor在字節碼中的表示
java代碼:
對應的字節碼:
syncsTask方法字節代碼:
可以分析出,同步語句塊的實現依賴的是monitorenter與monitorexit指令.
monitorenter指向代碼塊的開始位置,首先獲取printStream這個類,傳入參數,執行方法.
monitorexit指明同步代碼塊的結束位置.
當執行monitorenter指令時,當前線程將試圖獲取對象鎖即Object(沒聽清,音譯)所對應的持有權,當Object的Monitor進入計數器的count爲0時,線程就可以成功的獲取到Monitor,並將計數器設置爲1表示取鎖成功,如果我們當前線程在之前已經擁有了objectMonitor的持有權,它可以重入這個monitor.假如其它線程已經先於當前線程擁有ObjectMonitor的所有權,那麼當前線程將會被阻塞在這裏,直到持有該鎖的線程執行完畢,即monitorexit被執行,執行線程將釋放Monitor鎖,並設置計數器爲0,其它線程將有機會持有Monitor.
爲了保證該方法異常時也能正確執行,編譯器會自動生成一個異常處理器,包含另一個monitorexit方法.
什麼是重入:
重入代碼如下:
syncTask方法字節代碼:
可以看到它並沒有monitorenter和monitorexit,字節碼也比較短,因爲方法集的同步時隱式的,即無需通過字節碼指令來控制.
我們可以看到有ACC_SYNCHORONIZED一個訪問標誌,用來區分一個方法是否是同步方法,當方法調用時此訪問標誌會被檢測,當被設置時線程將會持有monitor,然後再執行方法,最後在方法無論是正常還是異常完成的情況下執行monitorexit.
第三章 鎖的優化
爲什麼會對synchronized嗤之以鼻?
java6之後,synchronized性能得到了極大的提升.
3.1 自旋鎖與自適應自旋鎖
3.1.1 自旋鎖
許多情況下,共享數據的鎖定狀態持續時間短,切換線程不值.
可以讓沒獲取到monitor鎖的線程在門外等待一會兒,但不放棄CPU的執行時間.即通過讓線程執行忙循環(自旋)等待鎖的釋放,不讓出CPU.不像sleep一樣會讓出CPU的執行時間.
缺點:若鎖被其它線程長時間佔用,會白白等很久,會帶來許多性能上的開銷.所以可以設置如果在一定時間內沒有等到鎖,就應該掛起線程了.
3.1.2 自適應自旋鎖
由於每次線程需要等待的時間不是固定的,所以想設定時間比較合理是很困難的,所以我們需要自適應自旋鎖.
自旋的次數不再固定,由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定.
如果對於某個鎖,自旋經常獲取到鎖,那麼就增加等待時間,如果對於某個鎖,自旋很少獲取到鎖,那在以後獲取這個鎖時可能省略到這個過程,以避免浪費時間.
3.2 鎖消除
java 虛擬機在JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖.
可以看到appen是加鎖的,但是這個鎖永遠不會發生競爭,sb屬於本地變量沒有return,所以這個鎖可以消除.
3.3 鎖粗化
我們有時會將鎖限制在儘量小的範圍,這樣需要同步的數量儘可能的變小,使等待鎖的線程儘可能塊的拿到鎖.
但是如果一連串系類操作都會同一個對象反覆加鎖和解鎖,甚至加鎖操作出現在循環體中,那即使沒有鎖競爭,頻繁的進行互斥鎖操作也會導致不必要的性能浪費,此時我們可以礦大枷鎖的範圍,避免反覆加鎖和解鎖,
如下圖所示,JVM會把加鎖的範圍到循環的外部,使整個操作只需要加鎖一次.
第四章 synchronized的四種狀態
無鎖就是沒有鎖,重量級鎖我們剛纔說過,所以我們這裏主要介紹偏向鎖和輕量級鎖.
4.1 偏向鎖
應用在鎖不存在多線程競爭,總是由同一線程多次獲得.
不適用於鎖競爭比較激烈的多線程場合.
4.2 輕量級鎖
偏向鎖失敗後會膨脹爲輕量級鎖.
具體過程:
解鎖的過程:
鎖的內存語義:
總的來說:
總結:對象頭裏有mark word,markword裏存儲着這個鎖的狀態,
當多個線程競爭一個鎖時,有幾種不同的情況:
偏向鎖,
輕量級鎖,
重量級鎖
第五章 Synchronized和ReentrantLock的區別
在java5以前,Synchronized是僅有的同步手段,從java5開始,提供了ReentrantLock(再入鎖)的實現,它的語義和synchronized基本相同,通過代碼直接調用lock方法去獲取,代碼編寫也更加靈活.
ReentrantLock的特點:
查看ReentrantLock源碼,進入其lock方法中.
繼續點進其acquire方法中,再點進其acquireQueued方法中.
我們發現acquireQueued在類AbstractQueuedSynchronizer中,AbstractQueuedSynchronizer是隊列同步器,簡稱AQS,它是java併發用來構建鎖或其它同步組件的基礎框架,是JUCpacakge的核心,一般使用AQS的方式是繼承.
像ReentrantLock這些子類是通過AQS實現的抽象方法來管理這些同步狀態,一種同步結構往往是可以利用其它的結構去實現的,但是對某種同步結構的傾向會導致複雜晦澀的實現邏輯,所以把基礎的同步相關的操作抽象在AQS中了,利用AQS爲我們提供同步結構實現了範本.
AQS內部的數據和方法可以簡單拆分爲一個volatile數組成員表徵狀態即state,一個先進先出的等待隊列,各種針對CAS的基礎操作方法,以及期望各種同步結構去實現的acquire,release方法,利用AQS實現一個同步結構至少要實現兩個基本類型的方法,acquire操作,獲取資源的獨佔權,release操作,用來釋放對某個資源的獨佔.
5.1 ReentrantLock公平性設置
傳入true表示是公平鎖.
建議程序確實有公平需要的時候再用,不用會有額外開銷.
具體代碼:
public class ReentrantLockDemo implements Runnable{
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run(){
while (true){
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
Thread.sleep(1000);
} catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread thread1 = new Thread(rtld);
Thread thread2 = new Thread(rtld);
thread1.start();
thread2.start();
}
}
公平鎖輸出:
非公平鎖輸出:
ReentrantLock相比Synchronized因爲可以像普通對象一樣使用,所以可以利用它來提供各種便利的方法,進行精細的同步操作.甚至可以實現Synchronized難以表達的用例.
5.2 wait/notify/notifyAll對象化
ReentrantLock將Synchronized轉變成了可控的對象,是不是也能將前面講的wait/notify/notifyAll對象化?
位於JUC包中的locks.Condition做到了這一點.Condition最爲典型的應用場景就是標準類庫中的ArrayBlockingQueue,ArrayBlockingQueue是數組實現的線程安全的,有界的阻塞隊列,線程安全是指ArrayBlockingQueue內部通過互斥鎖保護競爭資源,其互斥鎖是通過ReentrantLock來實現的,實現了多線程對競爭資源的互斥訪問,而有界則指的是ArrayBlockingQueue對應的數組是有界限的,阻塞隊列是指多線程訪問競爭資源時,當競爭資源已被某線程獲取時,其它要獲取該資源的線程要阻塞等待.
ArrayBlockingQueue與condition是組合的關係,ArrayBlockingQueue內部有兩個condition對象,一個是notEmpty,一個是notFull,而且condition依賴於ArrayBlockingQueue存在,通過condition可以實現對ArrayBlockingQueue更精確的訪問,
可以看其構造函數,notEmpty與notFull都是同一個lock創建出來的,然後使用在特定的操作中.
notEmpty用在take方法中,用在當count=0的時候,滿足當隊列爲空時,試圖take線程的正確行爲,應該是等待有新的消息加入到隊列纔去做返回,如同之前的future,使用notEmpty就可以優雅的實現等待邏輯,take操作的前提是要保證消息入隊,只有隊列有消息才觸發去取走.
看一下入隊的方法.可以看到一旦有消息被放入隊列當中,count便會++,notEmpty會調用signal函數去通知等待的線程,signal如同之前說的notify,此時非空條件就會滿足,take就能取到對應的東西了.
通過signal和await的組合,ArrayBlockingQueue就能優雅的完成了條件判斷和通知等待線程,非常順暢完成了狀態流轉.
注意到notEmpty是從new condition出來的,發現condition來自於ConditionObject,ConditionObject來源於AbstractQueuedSynchronizer也就是AQS框架,就是將我們的wait,notify,notifyall等操作轉換成了相對應的對象,將複雜而晦澀的同步操作轉變爲直觀可控的對象行爲.
總結:
針對最後一條我們去找到park方法.
首先找到ReentrantLock類的lock方法,點擊進acquire方法中.再點擊進acquireQueued方法,
在acquireQueued中找到parkAndCheckInterrupt方法
點擊進去,發現其調用的是LockSupport方法,再點進去發現其調用的是U.park方法.
再點進去,最後發現park方法位於unsafe類裏,unsafe是一個類似於後門的工具,可以在任意內存位置處讀寫數據.另外unsafe還支持一些CAS的操作.