鎖優(yōu)化建議
代碼層面上對(duì)鎖進(jìn)行優(yōu)化
減小鎖持有時(shí)間
在鎖的競(jìng)爭(zhēng)過程中,單個(gè)線程對(duì)鎖的持有時(shí)間與系統(tǒng)性能有著很大關(guān)系。如果線程持有鎖的時(shí)間很長(zhǎng),那么鎖得競(jìng)爭(zhēng)程度就會(huì)很大,這個(gè)很容易理解,就不解釋了,貼個(gè)減小鎖粒度的代碼吧。
//優(yōu)化前
public synchronized void syncMethod(){
//比較耗時(shí)但沒必要同步的耗時(shí)操作
otherOperate1();
//只有這部分需要同步
metextMethod();
//比較耗時(shí)但沒必要同步的耗時(shí)操作
otherOperate2();
}
//優(yōu)化后
public void syncMethod(){
otherOperate1();
synchronized(this){
metextMethod();
}
otherOperate2();
}
減小鎖粒度
對(duì)于hashmap來講,它本身是非線程安全的,我們當(dāng)然可以直接對(duì)整個(gè)hashmap加鎖,這樣做的話加鎖粒度就太大。hashmap本身屬于數(shù)組+鏈表(或紅黑樹)來實(shí)現(xiàn)的,如果我們對(duì)于hashmap的操作僅限于每個(gè)數(shù)組位的鏈表來加鎖,那假定默認(rèn)數(shù)組長(zhǎng)度為16,我們理論上可以同時(shí)支持16個(gè)線程的并發(fā)訪問這個(gè)hashmap(假定每個(gè)線程都修改不通的鏈表),這樣的話加鎖粒度就變小了,可以參考ConcurrentHashMap的實(shí)現(xiàn)。
讀寫分離鎖來替換獨(dú)占鎖
在讀多寫少的場(chǎng)合,讀與讀之間的鎖等待顯然是沒有必要的,那么讀讀操作不等待的情況下使用讀寫鎖來替換獨(dú)占鎖顯然是能夠提高系統(tǒng)性能的。
鎖分離
讀寫鎖也是其中一種鎖分離的思想。當(dāng)然可以根據(jù)程序功能的特點(diǎn),使用類似的分離思想,比如當(dāng)兩種操作互不影響,鎖就可以分離,比如LinckBlockingQueue的take和put,take從頭部取,put從尾部放入。
鎖粗化
線程獲取鎖操作和釋放鎖操作都會(huì)耗時(shí)的,當(dāng)某個(gè)線程連續(xù)對(duì)同一鎖進(jìn)行請(qǐng)求和釋放的操作時(shí),我們就可以整合成對(duì)一次鎖的請(qǐng)求。比如以下demo,在for循環(huán)中頻繁獲得鎖。
public void demoMethod(){
for(int i=0;i<COUNT;i++){
synchronized(lock){
//do sth
//這種情況我們就可以把加鎖放到for循環(huán)外邊
}
}
}
JVM層面上對(duì)鎖的優(yōu)化
代碼層面上的鎖優(yōu)化是coder可控的,但是JVM層面是我們不可控的,但是一樣不能阻止我們了解一下幾種在JVM層面上對(duì)鎖的優(yōu)化。
鎖偏向
鎖偏向是一種對(duì)價(jià)鎖操作的優(yōu)化手段。它的核心思想是:如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式。當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無需再做任何同步操作,這樣就節(jié)省了大量有關(guān)鎖申請(qǐng)的操作,從而提高程序性能。當(dāng)然,如果競(jìng)爭(zhēng)比較激烈的場(chǎng)合,最有可能的情況就是每次都是不同的線程來請(qǐng)求相同的鎖,這樣偏向模式就失效了,這種情況下還不如不啟用偏向鎖。JVM參數(shù)-XX:+UseBiasedLocking開啟偏向鎖。
偏向鎖理解:比方有個(gè)富翁有2個(gè)兒子,有個(gè)兒子(A)在外邊不經(jīng)常回來,有個(gè)兒子(B)頻繁在家出入。但是房子鑰匙只有富翁才有,這種情況下,B要進(jìn)屋每次都要去問富翁拿鑰匙(獲取鎖),然后出房子的時(shí)候又要交回給富翁鑰匙(釋放鎖)。偏向鎖就是當(dāng)B拿到鎖之后,以后是B來獲取鎖的情況很大,那么富翁就將鑰匙偏向B,即給B鑰匙之后,B就不用給富翁鑰匙了,這樣每次B進(jìn)出都不用再向富翁請(qǐng)求鑰匙和歸還鑰匙。直到有一天A回來了,A去找富翁拿鑰匙的時(shí)候(產(chǎn)生了鎖競(jìng)爭(zhēng),偏向鎖失敗),這個(gè)時(shí)候富翁就會(huì)消除偏向鎖,將B的鎖膨脹為輕量級(jí)鎖。
輕量級(jí)鎖
偏向鎖失敗后,虛擬機(jī)并不為立即掛起線程。它還會(huì)使用一種成為輕量級(jí)鎖的優(yōu)化手段。它只是簡(jiǎn)單地將對(duì)象頭部作為指針,指向持有鎖的線程堆棧內(nèi)部,來判斷一個(gè)線程是否持有對(duì)象鎖。如果獲得輕量級(jí)鎖成功,則順利進(jìn)入臨界區(qū),否則表示其他縣城先搶到了鎖,當(dāng)前線程的鎖請(qǐng)求膨脹為重量級(jí)鎖。
自旋鎖
鎖膨脹后,虛擬機(jī)為了避免線程真是IDE在操作系統(tǒng)層面掛起,虛擬機(jī)還會(huì)做最后的努力,自旋。由于當(dāng)前線程暫時(shí)無法獲得所,但是什么時(shí)候可以獲得鎖是一個(gè)未知數(shù),CPU掛起線程和恢復(fù)線程是需要消耗時(shí)間片的,所以假定我們?cè)跇O少時(shí)鐘周期后就可以得到鎖,那么CPU掛起線程可能是一種得不償失的操作。因此,系統(tǒng)會(huì)進(jìn)行一次賭注:它假設(shè)在不久的將來,線程可以得到這把鎖,因此虛擬機(jī)會(huì)讓當(dāng)前線程做有限個(gè)空循環(huán)。在若干次的循環(huán)后,如果可以得到鎖,那就進(jìn)入臨界區(qū),否則,將線程掛起(畢竟自旋過多還是沒法得到鎖的話還不如掛起),自旋的次數(shù)有算法來確定,如果當(dāng)前線程在自旋后獲得了鎖,那么系統(tǒng)會(huì)認(rèn)為在下次的自選中它也會(huì)獲得鎖,就會(huì)增大自旋的次數(shù)。
鎖消除
鎖消除是一種更徹底的鎖優(yōu)化。JIT編譯的時(shí)候,通過對(duì)上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖。比如我們?cè)谑褂脙?nèi)置API的時(shí)候,如StringBuffer等。如下代碼所示:
public String createString(){
StringBuffer sb = new StringBuffer();
sb.append("xxx");
return sb.toString();
}
在如上的方法中,由于sb是局部變量,局部變量是在線程棧上分配的,屬于線程私有的。因此不可能被其他線程訪問。在這種情況下,JVM就會(huì)將這些無用的所操作去除。
鎖消除會(huì)涉及逃逸分析。以上方法中變量sb顯然沒有逃逸出當(dāng)前作用域,以此為基礎(chǔ)才能將加鎖操作去除。
逃逸分析必須在-server模式下進(jìn)行,-XX:+DoEscapeAnalysis參數(shù)打開逃逸分析。使用-XX:+EliminateLocks參數(shù)打開鎖消除。
ThreadLocal
ThreadLocal是另一種解決線程安全的方式。它旨在讓每個(gè)線程都擁有一份屬于自己的對(duì)象,解決多個(gè)線程共享一個(gè)變量帶來的問題。它為每個(gè)線程分配不同的對(duì)象,僅僅是起到了容器的作用,接下來看一下JDK具體的實(shí)現(xiàn)方式。
// ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
....
}
}
//Thread.java
//Thread類中有個(gè)ThreadLocalMap的對(duì)象
ThreadLocal.ThreadLocalMap threadLocals = null;
我們來看set方法,set的時(shí)候首先會(huì)獲得當(dāng)前線程 t ,然后以當(dāng)前線程 t 為key獲得當(dāng)前線程的ThreadLocalMap,說白了就是Thread類中的全局變量threadLocals,可以看getMap方法,里面直接就返回了t.threadLocals。這是一個(gè)ThreadLocalMap對(duì)象,如果這個(gè)值為null,那表示還未初始化,會(huì)通過createMap()創(chuàng)建一個(gè)ThreadLocalMap對(duì)象。如果已經(jīng)存在,那么會(huì)直接將value設(shè)置進(jìn)去。其中key為ThreadLocal本身(this)。
繼續(xù)來看get方法,get方法跟set方法獲取threadLocalMap對(duì)象的邏輯是一樣的。如果獲取到的對(duì)象為null,那說明還未初始化,通過setInitialValue方法初始化,并返回一個(gè)默認(rèn)值(默認(rèn)值是在該方法中通過initalValue方法初始化的,可以通過子類覆蓋該方法,返回自定義的默認(rèn)值)。如果返回的ThreadLocalMap對(duì)象不為null,說明已經(jīng)存在值了, 因此直接以ThreadLocal對(duì)象作為key,獲取該對(duì)象中的值(set方法存入的時(shí)候也是以ThreadLocal作為key的),然后強(qiáng)轉(zhuǎn)為合適的類型返回給當(dāng)前線程。
這里有個(gè)疑問:ThreadLocal在操作線程變量ThreaLocalMap對(duì)象set值的時(shí)候每次都是以自身this(ThreadLocla)為key。我們知道這個(gè)ThreadLocal一般全局只有一個(gè),多個(gè)線程共享,在這種情況下,每個(gè)線程的中的ThreadLocalMap對(duì)象始終都只會(huì)存在一個(gè)k-v,k=threadLocal,v為泛型對(duì)象(可以通過以下代碼進(jìn)行調(diào)試,會(huì)發(fā)現(xiàn)每次set的key都是同一個(gè),當(dāng)然也很好理解,并不用調(diào)試)。既然這樣,那Thread為什么要用一個(gè)ThreadLocalMap對(duì)象呢,直接用Object不是更好么(每一次的方法調(diào)用都會(huì)消耗CPU時(shí)間片,如果是Object的話直接返回會(huì)快些)?反正在get的時(shí)候都會(huì)涉及到強(qiáng)轉(zhuǎn)。資料解釋的是:我們?cè)谑褂镁€程池的時(shí)候,每個(gè)線程是可能不會(huì)被銷毀,這樣ThreadLocalMap就可能導(dǎo)致內(nèi)存泄漏,ThreadLocalMap內(nèi)的Entry是弱引用,當(dāng)外部強(qiáng)引用ThreadLocal對(duì)象被回收時(shí),ThreadLocalMap的key就為null了,value也就會(huì)被回收,防止了內(nèi)存泄漏。但是應(yīng)用一般情況只有一個(gè)ThreadLocal,這個(gè)對(duì)象在應(yīng)用啟動(dòng)過程中就不可能會(huì)被回收(手動(dòng)除外,設(shè)置threadLocal=null或者重新對(duì)其賦值,就會(huì)回收掉原來那個(gè)),那這么做的意義在哪?或者說這么做的優(yōu)點(diǎn)在哪?
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void threadLocal() {
Runnable r = () -> {
threadLocal.set(1);
threadLocal.set(2);
threadLocal.set(3);
};
new Thread(r).start();
}
無鎖(CAS)
用CAS(Compare and Swap)比較交換來實(shí)現(xiàn)無鎖,它對(duì)死鎖天生免疫,它沒有鎖競(jìng)爭(zhēng)帶來的開銷,比基于鎖的方式擁有更優(yōu)越的性能。但是對(duì)于應(yīng)用來講,如果業(yè)務(wù)邏輯很復(fù)雜,會(huì)極大的增加無鎖的編程難度。
CAS有點(diǎn)類似于數(shù)據(jù)庫的樂觀鎖,只不過CAS會(huì)在失敗后再次嘗試,直到嘗試成功。具體的細(xì)節(jié)就不講了比較好理解。
無鎖的線程安全類有AtomicInteger,AtomicIntegerArray等等,他們都在java.util.concurrent.atomic包下。這種原子類存在ABA問題。不解釋,直接看圖。

這種情況在過程不重要的時(shí)候不算是什么大問題,比如計(jì)算操作,這種操作不會(huì)引起結(jié)果的改變,但是會(huì)在其他一些和對(duì)象變化過程有關(guān)的場(chǎng)景,這些普通版的小兵就無能為力了。這個(gè)時(shí)候就要使用帶時(shí)間戳的AtomicStampedReference類了,不解釋咯。
對(duì)無鎖(CAS)的解釋比較少,但是了解數(shù)據(jù)庫的樂觀鎖的,都能夠很好的理解這個(gè)東西,也比較好理解,Good Luck!。
over ...