一孽水、鎖在Java虛擬機中的實現(xiàn)與優(yōu)化
1.1 偏向鎖
偏向鎖是JDK 1.6 提出的一種鎖優(yōu)化方式躏救。其核心思想是,如果程序沒有競爭姜凄,則取消之前已經(jīng)取得鎖的線程同步操作。也就說趾访,若某一鎖被線程獲取后态秧,便進入偏向模式,當線程再次請求這個鎖時扼鞋,無需進行相關的同步操作申鱼,從而節(jié)省了操作時間。如果在此之前有其他線程進行了鎖請求藏鹊,則鎖退出偏向模式润讥。在JVM中使用-XX:+UseBiasedLocking可以設置啟用偏向鎖。
當鎖對象處于偏向模式時盘寡,對象頭會記錄獲取鎖的線程
[JavaThread* | epoch | age | 1 | 01]
這樣楚殿,當該線程再次嘗試獲得鎖時,通過Mark Word的線程信息就可以判斷當前線程是否持有偏向鎖竿痰。
偏向鎖在鎖競爭激烈的場合沒有太強的優(yōu)化效果脆粥,因為大量的競爭會導致持有鎖的線程不停地切換,鎖也很難一直保持在偏向模式影涉,此時变隔,使用鎖偏向不僅得不到性能的優(yōu)化,反而有可能降低系統(tǒng)性能蟹倾。
1.2 輕量級鎖
如果偏向鎖失敗匣缘,Java虛擬機會讓線程申請輕量級鎖。輕量級鎖在虛擬機內部鲜棠,使用一個稱謂BasicObjectLock的對象實現(xiàn)肌厨,這個對象內部由一個BasicLock對象和一個持有該鎖的Java對象指針組成。BasicObjectLock對象放置在Java棧的棧幀中豁陆。在BasicLock對象內部還維護者displaced_header字段柑爸,他用于備份對象頭部的Mark Word。
當一個線程持有一個對象的鎖時盒音,對象頭部Mark Word如下:
[ptr | 00] locked
末尾兩位比特為00表鳍,整個Mark Word為指向BasicLock對象的指針。由于BasicObjectLock對象在線程棧中祥诽,因此該指針必然指向持有該鎖的線程椘┦ィ空間。當需要判斷某一線程是否持有該對象鎖時原押,也只需簡單的判斷對象頭的指針是否在當前線程的棧地址范圍即可胁镐。同時,BasicLock對象的displaced_header字段,備份了元對象的Mark Word內存盯漂。BasicObjectLock對象的obj字段則指向該對象颇玷。
1.3 鎖膨脹
當輕量級鎖失敗,虛擬機就會使用重量級鎖就缆。在使用重量級鎖時帖渠,對象的Mark Word如下:
[ptr | 10] monitor
末尾的2比特標記位被置為10。整個Mark Word表示指向monitor對象的指針竭宰。
1.4 自旋鎖
鎖膨脹后空郊,進入ObjectMonitor的enter(),線程很可能會在操作系統(tǒng)層面被掛起切揭,這樣線程上下文切換的性能損失就比較大狞甚。因此,在鎖膨脹后廓旬,虛擬機會做最后的爭取哼审,希望線程可以盡快進入臨界區(qū)而避免被操作系統(tǒng)掛起。一種較為有效的手段就是使用自旋鎖孕豹。
自旋鎖可以使線程在沒有取得鎖時涩盾,不被掛起,而轉而去執(zhí)行一個空循環(huán)(即所謂的自旋)励背,在若干個空循環(huán)后春霍,線程如果可以獲得鎖,則繼續(xù)執(zhí)行叶眉。若線程依然不能獲得鎖址儒,才會被掛起。
使用自旋鎖后衅疙,線程被掛起的幾率相對減少离福,線程執(zhí)行的連貫性相對加強。因此炼蛤,對于那些鎖競爭不是很激烈,鎖占用時間很短的并發(fā)線程蝶涩,具有一定的積極意義理朋,但對于鎖競爭激烈,單線程鎖占用時間長的并發(fā)程序绿聘,自旋鎖在自旋等待后嗽上,往往依然無法獲得對應的鎖,不僅僅白白浪費了CPU時間熄攘,最終還是免不了執(zhí)行被掛起的操作兽愤,反而浪費了系統(tǒng)資源。
在JDK 1.6 中,Java虛擬機提供-XX:+UseSpinning參數(shù)來開啟自旋鎖浅萧,使用-XX:PreBlockSpin參數(shù)來設置自旋鎖的等待次數(shù)逐沙。
在JDK 1.7中,自旋鎖的參數(shù)被取消洼畅,虛擬機不再支持由用戶配置自旋鎖吩案。自旋鎖總是會執(zhí)行,自旋次數(shù)也由虛擬機自行調整帝簇。
1.5 鎖消除
鎖消除是Java虛擬機在JIT編譯時徘郭,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖丧肴。通過鎖消除残揉,可以節(jié)省毫無意義的請求鎖時間。
二芋浮、鎖在應用層的優(yōu)化思路
2.1 減少鎖持有時間
public Matcher matcher(CharSequence input) {
if (!compiled) {
synchronized(this) {
if (!compiled)
compile();
}
}
Matcher m = new Matcher(this, input);
return m;
}
2.2 減小鎖粒度
典型的場景就是ConcurrentHashMap類的實現(xiàn)抱环。ConcurrentHashMap將整個HshMap分成若干段(Segment),每個段都是一個子HashMap途样。
如果需要在ConcurrentHashMap中增加一個新的表項江醇,并不是將整個HashMap加鎖,而是首先根據(jù)hashcode得到該表項應該被存放到哪個段中何暇,然后對該段加鎖陶夜,并完成put()操作。在多線程環(huán)境中裆站,如果多個線程同時進行put()操作条辟,只要被加入的表項不存放在同一個段中,則線程間可以做到真正的并行宏胯。
2.3 鎖分離
鎖分離是減小鎖粒度的一個特例羽嫡。他依據(jù)應用程序的功能特點,將一個獨占鎖分成多個鎖肩袍。一個典型的案例就是java.util.concurrent.LinkedBlockingQueue的實現(xiàn)杭棵。
在LinkedBlockingQueue的實現(xiàn)中,take()函數(shù)和put()函數(shù)分別實現(xiàn)了從隊列中取得數(shù)據(jù)和往隊列中zeng增加數(shù)據(jù)的功能氛赐。雖然兩個函數(shù)都對當前隊列進行了修改操作魂爪,但由于LinkedBlockingQueue是基于鏈表的,因此艰管,兩個操作分別作用于隊列的前端和尾端滓侍,從理論上來說,兩者并不沖突牲芋。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
2.4 鎖粗化
通常情況下撩笆,為了保證多線程間的有效并發(fā)捺球,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后夕冲,應該立即釋放鎖氮兵。只有這樣,等待在這個鎖上的其他線程才能盡早的獲得資源執(zhí)行任務耘擂。但是胆剧,凡事都有一個度,如果對同一個鎖不停地進行請求醉冤、同步和釋放秩霍,其本身也會消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化蚁阳。
為此铃绒,虛擬機在遇到一連串連續(xù)的對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求螺捐,從而減少對鎖的請求同步次數(shù)颠悬。這個操作叫做鎖的粗化。
三定血、無鎖
可以使用yi'z一種稱為非阻塞同步的方法赔癌,這種方法不需要使用“鎖”(因此稱之為無鎖),但是依然能確保數(shù)據(jù)和程序在高并發(fā)環(huán)境下澜沟,保持多線程間的一致性灾票。
3.1 理解CAS
CAS算法的過程是這樣:它包含3個參數(shù)CAS(V,E,N)。V表示要更新的變量茫虽,E表示預期值刊苍,N表示新值。僅當V值=E值時濒析,才會將V的值設為N正什,如果V值和E值不同,則說明已經(jīng)有其他線程做了更新号杏,則當前線程什么都不做婴氮。最后,CAS返回當前V的真實值盾致。
3.2 原子操作
為了能讓CAS操作被Java應用程序充分使用莹妒,在JDK的java.util.concurrent.atomic包下,有一組使用無鎖算法實現(xiàn)的原子操作類绰上,主要有AtomicInteger、AtomicIntegerArray渠驼、AtomicLong蜈块、AtomicLongArray和AtomicReference等。他們分別封裝了對整數(shù)、整數(shù)數(shù)組百揭、長整型爽哎、長整型數(shù)組和普通對象的多線程安全操作。
3.3 LongAdder
在JDK 1.8中引入了LongAdder器一。結合減小鎖粒度與ConcurrentHashMap的實現(xiàn)课锌,我們可以想到一種對傳統(tǒng)AtomicInteger等原子類的改進思路。雖然在CAS操作中沒有鎖祈秕,但是像減小鎖粒度這種分離熱點的思路依然可以使用渺贤。一種可行的方案就是仿造ConcurrentHashMap,將熱點數(shù)據(jù)分離请毛。比如志鞍,可以將AtomicInteger的內部核心數(shù)據(jù)value分離成一個數(shù)組,每個線程訪問時方仿,通過哈希等算法映射到其中一個數(shù)字進行計數(shù)固棚,而最終的技術結果,則為這個數(shù)組的求和累加仙蚜。其中此洲,熱點數(shù)據(jù)value被分離成多個單元cell,每個cell獨自維護內部的值委粉,當前對象的實際值由所有的cell累計合成呜师,這樣,熱點就進行了有效的分離艳丛,提高了并行度匣掸。LongAdder正是使用了這種思想。
四氮双、理解Java內存模型
- 原子性
- 有序性
- 可見性
- Happens-Before原則