一、鎖在Java虛擬機中的實現與優化
1.1 偏向鎖
偏向鎖是JDK 1.6 提出的一種鎖優化方式。其核心思想是,如果程序沒有競爭,則取消之前已經取得鎖的線程同步操作。也就說,若某一鎖被線程獲取后,便進入偏向模式,當線程再次請求這個鎖時,無需進行相關的同步操作,從而節省了操作時間。如果在此之前有其他線程進行了鎖請求,則鎖退出偏向模式。在JVM中使用-XX:+UseBiasedLocking可以設置啟用偏向鎖。
當鎖對象處於偏向模式時,對象頭會記錄獲取鎖的線程
[JavaThread* | epoch | age | 1 | 01]
這樣,當該線程再次嘗試獲得鎖時,通過Mark Word的線程信息就可以判斷當前線程是否持有偏向鎖。
偏向鎖在鎖競爭激烈的場合沒有太強的優化效果,因為大量的競爭會導致持有鎖的線程不停地切換,鎖也很難一直保持在偏向模式,此時,使用鎖偏向不僅得不到性能的優化,反而有可能降低系統性能。
1.2 輕量級鎖
如果偏向鎖失敗,Java虛擬機會讓線程申請輕量級鎖。輕量級鎖在虛擬機內部,使用一個稱謂BasicObjectLock的對象實現,這個對象內部由一個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(),線程很可能會在操作系統層面被掛起,這樣線程上下文切換的性能損失就比較大。因此,在鎖膨脹后,虛擬機會做最后的爭取,希望線程可以盡快進入臨界區而避免被操作系統掛起。一種較為有效的手段就是使用自旋鎖。
自旋鎖可以使線程在沒有取得鎖時,不被掛起,而轉而去執行一個空循環(即所謂的自旋),在若干個空循環后,線程如果可以獲得鎖,則繼續執行。若線程依然不能獲得鎖,才會被掛起。
使用自旋鎖后,線程被掛起的幾率相對減少,線程執行的連貫性相對加強。因此,對於那些鎖競爭不是很激烈,鎖占用時間很短的並發線程,具有一定的積極意義,但對於鎖競爭激烈,單線程鎖占用時間長的並發程序,自旋鎖在自旋等待后,往往依然無法獲得對應的鎖,不僅僅白白浪費了CPU時間,最終還是免不了執行被掛起的操作,反而浪費了系統資源。
在JDK 1.6 中,Java虛擬機提供-XX:+UseSpinning參數來開啟自旋鎖,使用-XX:PreBlockSpin參數來設置自旋鎖的等待次數。
在JDK 1.7中,自旋鎖的參數被取消,虛擬機不再支持由用戶配置自旋鎖。自旋鎖總是會執行,自旋次數也由虛擬機自行調整。
1.5 鎖消除
鎖消除是Java虛擬機在JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。通過鎖消除,可以節省毫無意義的請求鎖時間。
二、鎖在應用層的優化思路
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類的實現。ConcurrentHashMap將整個HshMap分成若干段(Segment),每個段都是一個子HashMap。
如果需要在ConcurrentHashMap中增加一個新的表項,並不是將整個HashMap加鎖,而是首先根據hashcode得到該表項應該被存放到哪個段中,然后對該段加鎖,並完成put()操作。在多線程環境中,如果多個線程同時進行put()操作,只要被加入的表項不存放在同一個段中,則線程間可以做到真正的並行。
2.3 鎖分離
鎖分離是減小鎖粒度的一個特例。他依據應用程序的功能特點,將一個獨占鎖分成多個鎖。一個典型的案例就是java.util.concurrent.LinkedBlockingQueue的實現。
在LinkedBlockingQueue的實現中,take()函數和put()函數分別實現了從隊列中取得數據和往隊列中zeng增加數據的功能。雖然兩個函數都對當前隊列進行了修改操作,但由於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 鎖粗化
通常情況下,為了保證多線程間的有效並發,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能盡早的獲得資源執行任務。但是,凡事都有一個度,如果對同一個鎖不停地進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化。
為此,虛擬機在遇到一連串連續的對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數。這個操作叫做鎖的粗化。
三、無鎖
可以使用yi'z一種稱為非阻塞同步的方法,這種方法不需要使用“鎖”(因此稱之為無鎖),但是依然能確保數據和程序在高並發環境下,保持多線程間的一致性。
3.1 理解CAS
CAS算法的過程是這樣:它包含3個參數CAS(V,E,N)。V表示要更新的變量,E表示預期值,N表示新值。僅當V值=E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他線程做了更新,則當前線程什么都不做。最后,CAS返回當前V的真實值。
3.2 原子操作
為了能讓CAS操作被Java應用程序充分使用,在JDK的java.util.concurrent.atomic包下,有一組使用無鎖算法實現的原子操作類,主要有AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicLongArray和AtomicReference等。他們分別封裝了對整數、整數數組、長整型、長整型數組和普通對象的多線程安全操作。
3.3 LongAdder
在JDK 1.8中引入了LongAdder。結合減小鎖粒度與ConcurrentHashMap的實現,我們可以想到一種對傳統AtomicInteger等原子類的改進思路。雖然在CAS操作中沒有鎖,但是像減小鎖粒度這種分離熱點的思路依然可以使用。一種可行的方案就是仿造ConcurrentHashMap,將熱點數據分離。比如,可以將AtomicInteger的內部核心數據value分離成一個數組,每個線程訪問時,通過哈希等算法映射到其中一個數字進行計數,而最終的技術結果,則為這個數組的求和累加。其中,熱點數據value被分離成多個單元cell,每個cell獨自維護內部的值,當前對象的實際值由所有的cell累計合成,這樣,熱點就進行了有效的分離,提高了並行度。LongAdder正是使用了這種思想。
四、理解Java內存模型
- 原子性
- 有序性
- 可見性
- Happens-Before原則
