轉載:http://www.importnew.com/21353.html
1. 鎖優化的思路和方法
鎖優化的思路和方法有以下幾種:
- 減少鎖持有時間
- 減小鎖粒度
- 鎖分離
- 鎖粗化
- 鎖消除
1.1 減少鎖持有時間
public synchronized void syncMethod(){
othercode1();
mutextMethod();
othercode2();
}
像上述代碼這樣,在進入方法前就要得到鎖,其他線程就要在外面等待。
這里優化的一點在于,要減少其他線程等待的時間,所以,只需要在有線程安全要求的程序代碼上加鎖。
public void syncMethod(){
othercode1();
synchronized(this)
{
mutextMethod();
}
othercode2();
}
1.2 減小鎖粒度
將大對象(這個對象可能會被很多線程訪問),拆成小對象,大大增加并行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。
最最典型的減小鎖粒度的案例就是ConcurrentHashMap。
1.3 鎖分離
最常見的鎖分離就是讀寫鎖ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥。即保證了線程安全,又提高了性能。
讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。
比如LinkedBlockingQueue
從頭部取出數據,從尾部放入數據,使用兩把鎖。
1.4 鎖粗化
通常情況下,為了保證多線程間的有效并發,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能盡早的獲得資源執行任務。
但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利于性能的優化 。
舉個例子:
public void demoMethod(){
synchronized(lock){
//do sth.
}
//...做其他不需要的同步的工作,但能很快執行完畢
synchronized(lock){
//do sth.
}
}
這種情況,根據鎖粗化的思想,應該合并:
public void demoMethod(){
//整合成一次鎖請求
synchronized(lock){
//do sth.
//...做其他不需要的同步的工作,但能很快執行完畢
}
}
當然這是有前提的,前提就是中間的那些不需要同步的工作是很快執行完成的。
再舉一個極端的例子:
for(int i = 0; i < CIRCLE; i++){
synchronized(lock){
//...
}
}
在一個循環內不同得獲得鎖。雖然JDK內部會對這個代碼做些優化,但是還不如直接寫成:
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}
}
當然如果有需求說,這樣的循環太久,需要給其他線程不要等待太久,那只能寫成上面那種。如果沒有這樣類似的需求,還是直接寫成下面那種比較好。
1.5 鎖消除
鎖消除是在編譯器級別的事情。
在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作。
也許你會覺得奇怪,既然有些對象不可能被多線程訪問,那為什么要加鎖呢?寫代碼時直接不加鎖不就好了。
但是有時,這些鎖并不是程序員所寫的,有的是JDK實現中就有鎖的,比如Vector和StringBuffer這樣的類,它們中的很多方法都是有鎖的。當我們在一些不會有線程安全的情況下使用這些類的方法時,達到某些條件時,編譯器會將鎖消除來提高性能。
比如:
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
createStringBuffer("JVM", "Diagnosis");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
上述代碼中的StringBuffer.append是一個同步操作,但是StringBuffer卻是一個局部變量,并且方法也并沒有把StringBuffer返回,所以不可能會有多線程去訪問它。
那么此時StringBuffer中的同步操作就是沒有意義的。
開啟鎖消除是在JVM參數上設置的,當然需要在server模式下:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
并且要開啟逃逸分析。 逃逸分析的作用呢,就是看看變量是否有可能逃出作用域的范圍。
比如上述的StringBuffer,上述代碼中craeteStringBuffer的返回是一個String,所以這個局部變量StringBuffer在其他地方都不會被使用。如果將craeteStringBuffer改成
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
那么這個 StringBuffer被返回后,是有可能被任何其他地方所使用的(譬如被主函數將返回結果put進map啊等等)。那么JVM的逃逸分析可以分析出,這個局部變量 StringBuffer逃出了它的作用域。
所以基于逃逸分析,JVM可以判斷,如果這個局部變量StringBuffer并沒有逃出它的作用域,那么可以確定這個StringBuffer并不會被多線程所訪問,那么就可以把這些多余的鎖給去掉來提高性能。
當JVM參數為:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
輸出:
craeteStringBuffer: 302 ms
JVM參數為:
-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
輸出:
craeteStringBuffer: 660 ms
顯然,鎖消除的效果還是很明顯的。