Java的鎖機制主要分內(nèi)置鎖(隱式鎖)和顯式鎖烈涮。
內(nèi)置鎖
Java每個對象都有一個內(nèi)置的鎖對象朴肺,這些鎖對象不需要顯式地創(chuàng)建,可以直接對所有的Java對象加鎖坚洽,所以也叫隱式鎖戈稿。
雖然鎖對象是存在的,但是只有對鎖對象加鎖才能保證同步性讶舰。從現(xiàn)實角度看這是很容易理解的鞍盗,你擁有一把門鎖,但是你沒有鎖上绘雁,別人依舊能推開門橡疼。
內(nèi)置鎖的加鎖機制在JVM指令層面是通過monitorEnter和monitorExit來實現(xiàn)的,在操作系統(tǒng)層使用的是管程來實現(xiàn)互斥以及使用條件對象庐舟,在語言層面使用的是synchronized關(guān)鍵字欣除,使用synchronized聲明的方法,實例方法是對實例對象加鎖挪略,靜態(tài)方法是對class對象加鎖历帚,synchronized塊可以指定加鎖的對象滔岳,實現(xiàn)比同步方法更加細粒度的加鎖操作。
進入synchronized代碼塊即獲取了鎖挽牢,退出代碼塊(正常谱煤、異常)即釋放了鎖,這些操作是隱式的禽拔。
重入
獲取了鎖對象之后還能再次獲取同一個鎖刘离,說明這個鎖是可重入的《闷埽“重入”以為著鎖的操作粒度是線程而非調(diào)用硫惕。
重入的一種實現(xiàn)方法是為每個鎖關(guān)聯(lián)一個獲取計數(shù)值和一個所有者線程。計數(shù)值為0時野来,說明鎖沒有被線程持有恼除,當線程請求一個未被占用的鎖,JVM會記下鎖的持有者曼氛,并給鎖的計數(shù)器遞增一豁辉,如果同一個線程再次獲取這個鎖(未釋放鎖其他線程獲取不了),計數(shù)值自增舀患,釋放是遞減徽级,直到計數(shù)值為0,則線程釋放了鎖构舟。也就是說灰追,可重入鎖的釋放與重入要對應(yīng)堵幽。
顯式鎖
Java5.0新增了一個新的鎖機制:ReentrantLock(可重入鎖)狗超。這是一種顯式鎖,即需要顯式地創(chuàng)建對象的鎖對象朴下。
可重入鎖不是取代內(nèi)置鎖的努咐,它是在內(nèi)置鎖不適用時,作為一種高級功能使用的鎖殴胧。
Lock和ReentrantLock
ReentrantLock是Lock接口的實現(xiàn)渗稍。
Lock接口提供了一種無條件、可輪詢团滥、定時以及可中斷的鎖獲取操作竿屹,所有加鎖和解鎖操作都是顯式的。
ReentrantLock實現(xiàn)了Lock接口灸姊,并提供了與synchronized相同的互斥性和可見性拱燃。lock.lock()獲取鎖,和進入synchronized代碼塊有相同的內(nèi)存語義力惯,lock.unlock()釋放鎖碗誉,和離開synchronized代碼塊有相同的內(nèi)存語義召嘶。
非阻塞的鎖獲取
tryLock()方法提供了可定時和可輪詢的鎖獲取方法,可以有效地避免死鎖的發(fā)生哮缺。
可中斷的鎖獲取
lockInterruptibly方法在獲取鎖阻塞時可以被中斷弄跌,被中斷時會拋出InterruptedException。
非塊結(jié)構(gòu)的加鎖
鎖分段尝苇,使用多個鎖保護一個大的數(shù)據(jù)結(jié)構(gòu)铛只,使得鎖的粒度更小,降低鎖的競爭糠溜。內(nèi)置鎖也支持鎖分段格仲。
公平性
在ReentrantLock的構(gòu)造器可以指定公平性的設(shè)置,默認是非公平鎖诵冒。公平鎖的性能沒有非公平鎖高凯肋。
讀寫鎖ReadWriteLock
讀寫鎖內(nèi)部有兩個鎖對象,讀鎖和寫鎖汽馋,讀鎖共享寫鎖獨占侮东。
在讀寫鎖的加鎖策略中,允許多個讀同時執(zhí)行豹芯,但是同一時刻只能有一個寫悄雅。
ReentrantReadWriteLock
可重入讀寫鎖提供了可重入的加鎖語義。當訪問以讀取為主的數(shù)據(jù)結(jié)構(gòu)時铁蹈,讀寫鎖能提高伸縮性宽闲。
在公平的鎖中,如果一個鎖由讀線程持有握牧,二另一個線程請求寫入鎖容诬,那么其他讀線程都不能獲取讀鎖,知道寫線程使用完釋放了寫鎖沿腰。在非公平的鎖中览徒,線程獲取訪問許可的順序是不定的。寫線程降級為讀線程是允許的颂龙,但是反過來不可以习蓬,讀線程可以同時運行,如果同時請求寫鎖措嵌,容易造成死鎖躲叼。
public class ReadWriteMap<K,V>{
private final Map<K,V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();
public ReadWriteMap(Map<K,V> map){
this.map = map;
}
public V put(K key,V value){
w.lock();
try{
return map.put(key,value);
}finally{
w.unlock();
}
}
public V get(Object key){
r.lock();// 避免讀-寫沖突憨攒,能和寫鎖互斥
try{
return map.get(key);
}finally{
r.unlock();
}
}
}
鎖優(yōu)化
JVM為高效并發(fā)做出的鎖優(yōu)化技術(shù)赘被,為了在多線程之間更加高效地共享數(shù)據(jù)锹雏,以及減少數(shù)據(jù)競爭問題脸狸,從而提高程序執(zhí)行效率躬窜。
主要技術(shù)有:
- 適應(yīng)性自旋
- 鎖清除
- 鎖粗化
- 輕量級鎖
- 偏向鎖
自旋鎖和適應(yīng)性自旋
互斥同步對性能最大的影響是阻塞屏镊,阻塞會導(dǎo)致線程被掛起速妖,掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)來進行妹蔽。
為了讓線程等待鎖的時候不被操作系統(tǒng)掛起,讓線程執(zhí)行一個忙循環(huán)神帅,即自旋再姑。如果等待的鎖很快就被釋放了,那么自旋是很有意義的找御,但是如果一直沒有等到鎖釋放元镀,這段占用時間片的無實際意義的自旋操作只是白白浪費資源而已。
自適應(yīng)自旋是JVM會根據(jù)最近的等待時間自適應(yīng)地去安排自旋的時間霎桅。
鎖清除
鎖清除是編譯器的優(yōu)化措施栖疑,當JIT在運行時發(fā)現(xiàn)某個加鎖的操作只會被一個線程執(zhí)行,就會清除這個鎖滔驶。減少開銷遇革。
鎖粗化
如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖操作粗化到整個操作外層揭糕。
輕量級鎖
輕量級鎖是相對于使用系統(tǒng)信號量實現(xiàn)的傳統(tǒng)鎖而言的萝快。
輕量級鎖的用途是在沒有多線程競爭的前提下,減少傳統(tǒng)鎖的性能消耗著角。
對象模型
Java對象保存在堆內(nèi)存中揪漩。在內(nèi)存中,一個Java對象包含三部分:對象頭吏口、實例數(shù)據(jù)和對齊填充奄容。其中對象頭是一個很關(guān)鍵的部分,因為對象頭中包含鎖狀態(tài)標志产徊、線程持有的鎖等標志昂勒。
HotSpot虛擬機的對象頭分為兩部分信息,一部分保存對象自身的運行時數(shù)據(jù)囚痴,如hashcode叁怪、GC分代年齡等审葬,這部分數(shù)據(jù)長度在32位虛擬機是32位的64位則是64位深滚,官方稱之為Mark Word。這是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵涣觉。
另一部分存儲的是指向方法區(qū)的對象類型數(shù)據(jù)的指針痴荐,如果是數(shù)組對象的話,還會有一個額外的部分用于存儲數(shù)據(jù)長度官册。
在32位的Hotspot虛擬機中對象未被鎖定的情況下生兆,mark word的32位空間中25位存儲hashcode,4位存儲對象分代年齡,2位存儲鎖標志位鸦难,1為固定0根吁,在其他狀態(tài)下mw存儲的內(nèi)容如下表:
table-Hotspot虛擬機對象頭Mark Word
存儲內(nèi)容 | 標志位 | 狀態(tài) |
---|---|---|
hashcode和分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 重量級鎖定 |
空 | 11 | Gc標記 |
偏向線程ID、偏向時間戳合蔽、對象分代年齡 | 01 | 可偏向 |
輕量級鎖獲取執(zhí)行邏輯:
- 成功進入同步塊時(未鎖定)击敌,鎖標志位是01,虛擬機將在棧幀中建立一個叫鎖記錄(lock record)的空間拴事,存儲MarkWord的副本沃斤,叫做displaced mark word.
- jvm使用cas(compareAndSwitch)操作來嘗試將對象的MarkWord更新為指向lock record的指針。如果更新操作成功刃宵,說明當前線程成功獲取了鎖衡瓶,且對象的鎖標志位變成00。如果更新操作失敗了牲证,jvm會檢查對象的Mark Word是否指向當前線程的棧幀哮针,如果是則說明當前線程以及擁有了對象鎖(這個應(yīng)該發(fā)生在重入時),否則說明這個鎖被其他線程搶占了坦袍。
- 如果有兩條線程搶一個鎖诚撵,那輕量級鎖就不再有效了,會膨脹成重量級鎖键闺,鎖標志位變成10寿烟,后面等待鎖的過程也要變成阻塞狀態(tài)。
輕量級鎖解鎖邏輯: - 判斷對象的MW是否仍指向線程的鎖記錄辛燥,是就用CAS操作把對象當前的MW和lock record中的displaced MW替換回來筛武,替換成功就釋放鎖了。如果替換失敗了挎塌,說明有其他線程嘗試獲取該鎖徘六,就要在釋放鎖的同時,喚醒被掛起的線程榴都。
流程圖:java輕量級鎖原理.note
輕量級鎖是使用cas去代替?zhèn)鹘y(tǒng)的操作信號量的來同步的方式待锈。它能夠提升性能的前提是在同步周期沒有線程競爭鎖,實際上對于大部分鎖都是這個情況嘴高,這是一個經(jīng)驗數(shù)據(jù)竿音。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷拴驮,如果存在競爭春瞬,除了互斥量開銷還有CAS開銷,那就會更加慢套啤。
CAS 是處理器提供的非阻塞原子更新操作指令宽气。
偏向鎖
偏向鎖的目的是消除數(shù)據(jù)在無競爭情況下的 同步原語,進而提高性能。
輕量級鎖是避開使用OS的互斥量萄涯,偏向鎖是把整個鎖去除掉绪氛。偏向鎖的偏向是偏向第一個獲取它的線程,如果在接下來的執(zhí)行過程中涝影,該鎖沒有被其他線程獲取钞楼,則持有偏向鎖的線程將不需要再進行同步。
條件對象
有些情況下袄琳,在進入某個鎖的臨界區(qū)后询件,需要滿足一些條件才能繼續(xù)執(zhí)行,于是主動釋放鎖唆樊,且阻塞直到被通知再被喚醒宛琅。
條件對象是基于鎖機制上的一種協(xié)作機制,不滿足條件時在主動阻塞直到鎖條件對象發(fā)出通知才會被喚醒繼續(xù)執(zhí)行逗旁。
條件對象主要有內(nèi)置條件對象和顯式條件對象嘿辟,內(nèi)置條件對象時內(nèi)置鎖的條件對象,一個內(nèi)置鎖對象只能有一個條件對象片效;顯式條件對象可以支持多個條件對象红伦。
內(nèi)置條件對象
正如每個Java對象都有一個內(nèi)置的鎖對象,都可以作為一個鎖淀衣,每個對象同樣可以作為一個條件對象昙读。
Object類的wait()/wait(timeout)/wait(timeout,nanos),notify()和notifyAll()構(gòu)成內(nèi)置條件對象的API。
public class Object{
//...
public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException{//...}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException{
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
}
對象的內(nèi)置鎖和其內(nèi)部條件對象時相互關(guān)聯(lián)的膨桥,要調(diào)用對象X中條件隊列的任何一個方法蛮浑,必須持有X上的鎖。簡單的說只嚣,調(diào)用條件對象的方法沮稚,必須在此對象的鎖保護的同步塊內(nèi)部。
需要注意的是册舞,wait()方法會釋放當前持有的鎖,阻塞當前線程蕴掏,并等待直到超時(如果設(shè)置了超時,超時后會自己醒來)调鲸,如何線程被中斷(wait中的線程可被Interrupt且會拋出異常)或者被一個通知喚醒盛杰。喚醒線程后,線程從wait處繼續(xù)執(zhí)行(當然作為就緒態(tài)線程和其他線程一起競爭鎖)线得。
notify從等待條件的線程隊列中隨機喚醒一個饶唤,notifyAll是喚醒隊列中所有的。一般常用All贯钩,因為只喚醒一個可能會造成死鎖。
顯式條件對象
當對于一個鎖等待不同的條件時,內(nèi)置鎖就無法實現(xiàn)了角雷。顯式條件對象Condition支持多個條件祸穷。
Condition的定義:
public interface Condition{
void await() throws Interruptedexception;
boolean await(long timeout,TimeUnit unit) throws Interruptedexception;
long awaitNanos(long nanostimeout) throws Interruptedexception;
void awaitUninterruptibly();
boolean awaitUntil(Date deadline) throws Interruptedexception;
void singal();
void singalAll();
}
使用顯式條件變量的有界緩存
public class ConditionBoundedBuffer<T>{
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items = (T[])new Object[BUFFER_SIZE];
private int tail,head,count;
//阻塞直到notFull
public void put(T x) throws Interruptedexception{
lock.lock();
try{
while(count == items.length)
notFull.await();
item[tail] = x;
if(++tail == item.length)
tail=0;
++count;
notEmpty.singalAll();
}finally{
lock.unlock();
}
}
// 阻塞直到notEmpty
public T take() throws Interruptedexception{
lock.lock();
try{
while(count==0)
notEmpty.await();
T x = item[head];
if(++head == items.length)
head=0;
--count;
notFull.singalAll();
return x;
}finally{
lock.unlock();
}
}
}