鎖是最常見的同步方法之一,在高併發環境下,激烈的的鎖競爭導致程式的效能下降,因此我們有必要討論一些有關鎖的效能問題,以及一些注意事項,比如避免死鎖等。為了降低鎖的競爭導致程式效能下降的話,可以用以下建議提高一下效能。
1. 減少鎖持有時間
對於使用鎖進行併發控制的應用程式而言,在鎖競爭過程中,單個執行緒對鎖的持有時間與系統效能有著直接關係,如果執行緒持有鎖的時間越長,那麼鎖的競爭程度越激烈。以下面的**片段為例進行說明:
publicsynchronized
void
syncmethod()
在syncmethod()方法中,假設只有mutexmethod()方法是有同步需要的,而othercode1()方法和othercode2()方法並不需要做同步控制。如果這倆方法分別是重量級的方法的話,就會花費長時間的cpu,高併發情況下,同步整個方法會導致等待執行緒大量增加。因為執行緒進入該方法時獲得內部鎖,只有在所有任務執行完後才會釋放鎖,優化方案就是在只在必要是進行同步,這樣就能明顯減少執行緒持有鎖的時間,提高系統的吞吐量。
publicvoid
syncmethod2()
othercode2();
}
2. 減小鎖粒度
是指縮小鎖定物件的範圍,從而降低鎖衝突的可能性,進而提高系統的併發能力。在jdk中典型的使用場景就是concurrenthashmap,這個map內部細分成了若干個小的hashmap,稱之為段(segment),預設情況下是16段。
如果需要在concurrenthashmap中增加乙個新的表項,並不是將整個hashmap加鎖,而是首先根據hashcode得到該表項應該被存放在哪個段中,然後對該段加鎖,並完成put()方法操作。在多執行緒中,如果多個執行緒同時進行put()操作,只要被加入的表項不存放在同乙個段中,執行緒間便可以做到真正的並行。預設是16段,幸運的話,在可以接受16個執行緒同時插入,大大提公升吞吐量。下面的**就是其put()方法操作過程。第5~6行**根據key獲得對應段的序號 j,然後得到段 s,然後將資料插入給定的段中。
publicv put(k key, v value)
但是減小粒度會帶來乙個新問題,當系統需要全域性鎖的時候,其消耗的資源就很多了。比如要獲得concurrenthashmap的全域性資訊,就需要同時取得所有段的鎖方能順利實施。比如這個map的size()方法,返回concurrenthashmap全部有效表項之和,要獲取這個資訊需要取得所有子段的鎖,因此size()方法的**如下:
publicintsize()
sum = 0l;
size = 0;
overflow = false
;
for (int j = 0; j < segments.length; ++j)
}if (sum ==last)
break
; last =sum;}}
finally
}return overflow ?integer.max_value : size;
}
從上面**中可以看到size()首先嘗試無鎖求和,如果失敗了會嘗試加鎖的方法。只有類似於size()方法獲取全域性資訊的方法呼叫使用不頻繁時候,這種減小鎖粒度的方法才能在真正意義上提高系統的吞吐量。
3. 用讀寫分離鎖替換獨佔鎖
使用讀寫分離鎖readwritelock可以提高系統的效能,其實這是減小粒度的一種特殊情況,讀寫分離鎖是對系統功能點分割。因為讀操作本身不會影響資料的完整性和一致性,因此在理論上講,可以允許多執行緒之間同時讀,讀寫鎖正是實現了這個功能。所以在讀多寫少的場合使用讀寫鎖可以有效提公升系統的併發能力。
4. 鎖分離
將讀寫鎖的思想進一步延伸就是鎖分離了。讀寫鎖根據讀寫操作功能上的不同,進行有效的鎖分離。我們可以依據應用程式的功能特點,使用類似的分離思想,也可以對獨佔鎖進行分離。linkedblockingqueue的實現中,take()和put()分別實現了從佇列中取資料和增加資料的功能,雖然倆函式對佇列都有修改,但是linkedblockingqueue是基於鍊錶的,倆操作乙個作用與煉表頭,乙個作用於鍊錶尾部,從理論上講兩者不衝突的。使用獨佔鎖的話,這倆方法不可能實現真正的併發,他們彼此等待對方釋放鎖資源。jdk中分別用兩把鎖分離了take()和put()。
/**lock held by take, poll, etc
*/private
final reentrantlock takelock = new
reentrantlock();
/**wait queue for waiting takes
*/private
final condition notempty =takelock.newcondition();
/**lock held by put, offer, etc
*/private
final reentrantlock putlock = new
reentrantlock();
/**wait queue for waiting puts
*/private
final condition notfull = putlock.newcondition();
以上**片段定義了takelock和putlock,它們分別在take()和put()方法中使用,因此這倆方法就此相互獨立,這倆方法之間不存在鎖競爭關係,只需要take()和take()方法間、put()和put()方法間分別對takelock和putlock進行競爭,從而削弱了鎖競爭。take()方法實現如下:
public e take() throwsinterruptedexception
x =dequeue(); // 取得第乙個資料
c =count.getanddecrement();// 數量減一,原子操作,因此會和put()同時訪問count
if (c > 1)
notempty.signal(); // 通知其他take()方法操作
} finally
if (c ==capacity)
signalnotfull(); // 通知put()方法操作,已有空餘空間
return
x;}
put()方法實現如下:
publicvoid put(e e) throws
interruptedexception
enqueue(node); // 插入資料
c =count.getandincrement(); // 更新總數,變數c是count加1前的值
if (c + 1 notfull.signal(); // 有足夠空間,通知其他執行緒
} finally
if (c == 0)
signalnotempty(); // 插入成功後,通知take()方法取資料
}
5. 鎖粗化
通常情況下,為了保證多執行緒間的有效併發,會要求每個執行緒持有鎖的時間盡量短,使用資源後立即釋放鎖。只有這樣等待在這個鎖上的其他執行緒才能盡早的獲得資源執行任務,但是,如果對同乙個鎖不停進行請求、同步、釋放,其本身也會消耗系統資源,反而不利於效能優化。為此,虛擬機器在遇到一連串連續對同乙個鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這個操作叫鎖的粗化。
在開發過程中,可以在合理的場合進行鎖粗化,尤其是在迴圈內部請求鎖時,因為每次迴圈都有申請鎖和釋放鎖的操作,其實這完全沒必要的,在迴圈外加鎖就可以了。
多執行緒中記憶體分配的一些看法
近來開發乙個儲存系統,在開發的過程中遇到一些之前沒有考慮過的問題,讓人記憶比較深刻的算是多執行緒中反覆申請與釋放記憶體。由於這個系統需要很強的穩定性,這就讓我很痛苦了一兩個星期。大家都知道在c 中用new 來分配記憶體,用delete 來釋放記憶體,這看似沒有任何問題,因為new與delete總是成...
一些多執行緒的筆記
1.保護方法原子性的同時,也要注意保護方法中使用到的變數。下面這段 是否一定安全?public class counter public synchronized void add1 other method 不一定,如果在other method中也處理counter但是又沒有保護的情況下,會出現...
GML的一些看法
趨勢 更紮實的理論認識,公式推導,演算法層面 最新最酷的gnn應用 知識圖譜在變得越來越流行,知識圖譜應用到gnn 圖嵌入的新框架。1 更紮實的理論認識,公式推導,演算法層面 what graph neural networks cannot learn depth vs width graph n...