1. 程式人生 > >java鎖優化的方法與思路。

java鎖優化的方法與思路。

java程式開發中一旦用到鎖,就表示採用了阻塞形式的併發——一種最糟糕的併發等級。而鎖優化就是希望在高併發多執行緒程式當中將涉及到有鎖動作的相關程式碼儘可能的加以改進,使執行效率儘可能地得到提升。當然就算將這種用到了鎖的程式碼優化到極致,其效能也無法超越無鎖,畢竟鎖會導致執行緒掛起(相對來說相當耗時及浪費資源)。但是我們要想辦法讓這種損耗降到最低,這是鎖優化的出發點。

一般來說,java鎖優化有如下思路或方法:

  • 減少鎖持有時間
  • 減少鎖粒度
  • 鎖分離
  • 鎖粗化
  • 鎖消除

下面分別對鎖優化的各種方法或思路作詳細的介紹:

減少鎖持有時間

鎖在同一時間只能允許一個執行緒持有,其它想要佔用鎖的執行緒都得在臨界區外等待鎖的釋放,這個等待的時間根據實際的應用及程式碼寫法可長可短,比如下面的程式碼:

  1. publicsynchronizedvoid syncMethod(){
  2. noneLockedCode1();//不需要加鎖的程式碼
  3. needLockedMethed();//需要執行緒安全的程式碼
  4. noneLockedCode2();//不需要加鎖的程式碼
  5. }

在syncMethod方法中呼叫了三個方法,每個方法各代表一段程式碼塊,若其中只有一個方法needLockedMethed()需要執行緒安全,其它兩個方法就沒必要放到同步程式碼塊內執行,那麼就可以將上面的程式碼進行如下改進:

  1. publicvoid syncMethod(){
  2. noneLockedCode1();
  3. synchronized
    (this){
  4. needLockedMethed();
  5. }
  6. noneLockedCode2();
  7. }

這樣方法noneLockedCode1與noneLockedCode2的執行就不會佔用鎖的時間,減少了其它執行緒等待鎖的時長,因此,也就提高了程式的效能,使鎖的使用得到優化。

減少鎖粒度

加鎖可能是針對一個很重的物件(物件會被很多個執行緒加鎖),這時若將大物件拆成更小粒度的小物件,就可以增加程式的並行度,降低多執行緒間鎖的競爭,使加鎖的成功率得到提高,因而達到鎖優化的目的。

關於減少鎖粒度的一個重要例子ConcurrentHashMap的實現。

我們知道HashMap若要實現執行緒安全,可以這麼做:Collections.synchronizedMap(Map<K,V> m)

,該方法返回SynchronizedMap物件:

  1. publicstatic<K,V>Map<K,V> synchronizedMap(Map<K,V> m){
  2. returnnewSynchronizedMap<>(m);
  3. }

它的實現也很簡單,僅僅是將get與set方法進行了互斥的同步,實現程式碼如下:

  1. public V get(Object key){
  2. synchronized(mutex){
  3. return m.get(key);
  4. }
  5. }
  6. public V put(K key, V value){
  7. synchronized(mutex){
  8. return m.put(key, value);
  9. }
  10. }

這樣做會有什麼問題?

這裡的hashmap其實就是一個很重的物件,因為它裡面可能會儲存很多資料,當多個執行緒同時進來訪問的時候,不管是讀(get)還是寫(put),都要拿到互斥對(mutex),因此讀與寫會相互阻塞,也就是說SynchronizedMap其實只支援對其中存放的一個物件進行讀寫,這無疑會帶來很大的效能損耗,且map中資料越多、訪問map的執行緒越多,損耗的效能就越大。

相對來講,ConcurrentHashMap就是一個高效能的雜湊表,這個高效能僅僅是因為它做了一個減小鎖粒度的一個操作。在ConcurrentHashMap的源中,我們可以發現,它把整個Hashmap拆成了若干個小的segment,每一個segment都是一個小的hashmap,當有執行緒去操作裡面的資料時,實時上操作的是被拆分後的某個小的segment,從而使ConcurrentHashMap允許多個執行緒同時進入,因此增加了並行度,達到了鎖優化的目的。

鎖分離

如果對系統有讀和寫的要求,普通鎖(如syncronized)會導致讀阻塞寫、寫也會阻塞讀,同時讀讀與寫寫之間也會進行阻塞,鎖優化的目的是要想辦法使得阻塞儘可能的小,這裡讀寫鎖就會起來一定的優化作用。

讀寫鎖的基本思想是將讀與寫進行分離,因為讀不會改變資料,所以讀與讀之間不需要進行同步,其它讀寫、寫讀、寫寫之間的情況如下表:

讀鎖 寫鎖
讀鎖 可以訪問 不可訪問
寫鎖 不可訪問 不可訪問

表中可以看出,只要有寫鎖進入就需要做同步處理,但是對於大多數應用來說,讀的場景要遠遠大於寫的場景,因此一旦使用讀寫鎖,在讀多寫少的場景中,就可以很好的提高系統的效能,這就是鎖分離。鎖分離之後在讀鎖與讀鎖之間就不再是阻塞的併發了,而是無等待的併發,這種鎖優化方式將在一定場景下極大的提高系統的效能

鎖分離在java中應用延伸的一個例子就是LinkedBlockingQueue:

它充分利用熱點分離的思想,從頭部拿資料(讀),新增資料(寫)則在尾部,讀與寫這兩者操作的資料在不同的部位,因此可以同時進行操作,使併發級別更高,除非佇列或連結串列中只有一條資料。這就是讀寫分離思想的進一下延伸:只要操作不相互影響,鎖就可以分離。

鎖粗化

通常情況下,為了保證多執行緒間的有效併發,會要求每個執行緒持有鎖的時間儘可能短,但是大某些情況下,一個程式對同一個鎖不間斷、高頻地請求、同步與釋放,會消耗掉一定的系統資源,因為鎖的講求、同步與釋放本身會帶來效能損耗,這樣高頻的鎖請求就反而不利於系統性能的優化了,雖然單次同步操作的時間可能很短。鎖粗化就是告訴我們任何事情都有個度,有些情況下我們反而希望把很多次鎖的請求合併成一個請求,以降低短時間內大量鎖請求、同步、釋放帶來的效能損耗。

一種極端的情況如下:

  1. publicvoid doSomethingMethod(){
  2. synchronized(lock){
  3. //do some thing
  4. }
  5. //這是還有一些程式碼,做其它不需要同步的工作,但能很快執行完畢
  6. synchronized(lock){
  7. //do other thing
  8. }
  9. }

上面的程式碼是有兩塊需要同步操作的,但在這兩塊需要同步操作的程式碼之間,需要做一些其它的工作,而這些工作只會花費很少的時間,那麼我們就可以把這些工作程式碼放入鎖內,將兩個同步程式碼塊合併成一個,以降低多次鎖請求、同步、釋放帶來的系統性能消耗,合併後的程式碼如下:

  1. publicvoid doSomethingMethod(){
  2. //進行鎖粗化:整合成一次鎖請求、同步、釋放
  3. synchronized(lock){
  4. //do some thing
  5. //做其它不需要同步但能很快執行完的工作
  6. //do other thing
  7. }
  8. }

注意:這樣做是有前提的,就是中間不需要同步的程式碼能夠很快速地完成,如果不需要同步的程式碼需要花很長時間,就會導致同步塊的執行需要花費很長的時間,這樣做也就不合理了。

另一種需要鎖粗化的極端的情況是:

  1. for(int i=0;i<size;i++){
  2. synchronized(lock){
  3. }
  4. }

上面程式碼每次迴圈都會進行鎖的請求、同步與釋放,看起來貌似沒什麼問題,且在jdk內部會對這類程式碼鎖的請求做一些優化,但是還不如把加鎖程式碼寫在迴圈體的外面,這樣一次鎖的請求就可以達到我們的要求,除非有特殊的需要:迴圈需要花很長時間,但其它執行緒等不起,要給它們執行的機會。

鎖粗化後的程式碼如下:

  1. synchronized(lock){
  2. for(int i=0;i<size;i++){
  3. }
  4. }

鎖消除

鎖消除是發生在編譯器級別的一種鎖優化方式。
有時候我們寫的程式碼完全不需要加鎖,卻執行了加鎖操作。
比如,StringBuffer類的append操作:

  1. @Override
  2. publicsynchronizedStringBuffer append(String str){
  3. toStringCache =null;
  4. super.append(str);
  5. returnthis;
  6. }

從原始碼中可以看出,append方法用了synchronized關鍵詞,它是執行緒安全的。但我們可能僅線上程內部把StringBuffer當作區域性變數使用:

  1. package com.leeib.thread;
  2. publicclassDemo{
  3. publicstaticvoid main(String[] args){
  4. long start =System.currentTimeMillis();
  5. int size =10000;
  6. for(int i =0; i < size; i++){
  7. createStringBuffer("Hyes","為分享技術而生");
  8. }
  9. long timeCost =System.currentTimeMillis()- start;
  10. System.out.println("createStringBuffer:"+ timeCost +" ms");
  11. }
  12. publicstaticString createStringBuffer(String str1,String str2){
  13. StringBuffer sBuf =newStringBuffer();
  14. sBuf.append(str1);// append方法是同步操作
  15. sBuf.append(str2);
  16. return sBuf.toString();
  17. }
  18. }

程式碼中createStringBuffer方法中的區域性物件sBuf,就只在該方法內的作用域有效,不同執行緒同時呼叫createStringBuffer()方法時,都會建立不同的sBuf物件,因此此時的append操作若是使用同步操作,就是白白浪費的系統資源。

這時我們可以通過編譯器將其優化,將鎖消除,前提是java必須執行在server模式(server模式會比client模式作更多的優化),同時必須開啟逃逸分析:

  1. -server -XX:+DoEscapeAnalysis-XX:+EliminateLocks

其中+DoEscapeAnalysis表示開啟逃逸分析,+EliminateLocks表示鎖消除。

逃逸分析:比如上面的程式碼,它要看sBuf是否可能逃出它的作用域?如果將sBuf作為方法的返回值進行返回,那麼它在方法外部可能被當作一個全域性物件使用,就有可能發生執行緒安全問題,這時就可以說sBuf這個物件發生逃逸了,因而不應將append操作的鎖消除,但我們上面的程式碼沒有發生鎖逃逸,鎖消除就可以帶來一定的效能提升。

以上就是java鎖優化的5個方法或思想。