1. 程式人生 > >ava多執行緒程式設計-(13)- 關於鎖優化的幾點建議

ava多執行緒程式設計-(13)- 關於鎖優化的幾點建議

原文出自 : https://blog.csdn.net/xlgen157387/article/details/78363616


一、背景

《 Java多執行緒程式設計-(11)-從volatile和synchronized的底層實現原理看Java虛擬機器對鎖優化所做的努力》 這一篇文章中,我們大致介紹了Java虛擬機器對鎖優化所做的努力,提到了:偏向鎖、輕量級鎖、重量級鎖以及自旋鎖。

通過上面的學習,我們應該很清楚的知道了在多執行緒併發情況下如何保證資料的安全性和一致性的兩種主要方法:一種是加鎖,另一種是使用ThreadLocal。鎖是一種以時間換空間的方式,而ThreadLocal是一種以空間換時間的方式。而關於ThreadLocal的正確使用,以及不正確的使用會造成的OOM已經在前邊的文章中有所學習,下邊就鎖的問題在進一步探討一下。

二、為什麼還要進一步探討鎖

我們知道加鎖同步的時候,同一時刻只允許一個執行緒訪問臨界資源的。因此,在高併發的情況下激烈的鎖競爭以及上下文切換會導致程式的效能下降,就像並不是所有東西都是最優的一樣,同樣對於鎖來說也是有很多可以進行優化的地方。

三、有關鎖優化的幾點建議

1、減少鎖持有的時間

首先看一段程式碼:

public synchronized void syncMethod(){
	method1();
	mutextMethod(); //實際需要進行同步的方法
	method2();
}

  
  • 1
  • 2
  • 3
  • 4
  • 5

可以看出只有mutextMethod()

方法是有同步需求的,而method1()method2() 方法是不需要進行同步的,但是如果method1()、method2() 是兩個耗時的方法的話,那麼整個鎖持有的時間就會增加,很顯然是一種不合理的設計,正確的方式應該使用如下的方式:

public void syncMethod() {
        method1();
        synchronized (this) {
            mutextMethod(); //實際需要進行同步的方法
        }
        method2();
    }

  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這樣的話,只在有必要的時候進行同步,這樣就明顯減少了執行緒持有鎖的時間,從而提高系統的效能。

2、減小鎖粒度

減小鎖粒度是一種削弱多執行緒鎖競爭的有效方法。我們知道HashMap不是執行緒安全的而HashTable是執行緒安全的,但是我們在使用HashTable的時候,無論是進行讀還是進行寫操作都需要現獲取鎖,當有一個執行緒獲取鎖之後進行操作,其他的執行緒就必須進行阻塞等待,可見在高併發的情況下HashTable的效能會顯著下降。

為了解決HashTable效率低下的問題ConcurrentHashMap出現了!

HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的執行緒都必須競爭同一把鎖,假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

HashMap和ConcurrentHashMap鎖的區別:

這裡寫圖片描述

在預設的情況下,ConcurrentHashMap有16個Segment,也就是有16把鎖,這樣的話只要不同的執行緒獲取不同鎖鎖住某一個Segment,這樣的話就可以實現高併發的操作,這也是減小鎖粒度 的一個典型使用。

3、使用讀寫鎖替換獨佔鎖

前幾篇文章中,我們大致瞭解了可以使用ReentrantLock 實現執行緒的同步,ReentrantLock具有完全互斥排他的效果,即同一時間只能有一個執行緒在執行ReentrantLock.lock()之後的任務。

類似於我們集合中有同步類容器併發類容器,HashTable是完全排他的,即使是讀也只能同步執行,而ConcurrentHashMap就可以實現同一時刻多個執行緒之間併發。為了提高效率,ReentrantLock的升級版ReentrantReadWriteLock就可以實現效率的提升。

ReentrantReadWriteLock有兩個鎖:一個是與讀相關的鎖,稱為“共享鎖”;另一個是與寫相關的鎖,稱為“排它鎖”。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。

在沒有執行緒進行寫操作時,進行讀操作的多個執行緒都可以獲取到讀鎖,而寫操作的執行緒只有獲取寫鎖後才能進行寫入操作。即:多個執行緒可以同時進行讀操作,但是同一時刻只允許一個執行緒進行寫操作。

4、鎖分離

如果將讀寫鎖的思想進一步延伸,就是鎖分離。也就是說:只要操作互不影響,鎖就可以分離。最典型的一個例子就是LinkedBlockingQueue ,示意圖如下:

這裡寫圖片描述

LinkedBlockingQueue是基於連結串列的,take和put方法分別是向連結串列中取資料和寫資料,他們的操作一個是從佇列的頭部一個是佇列的尾部,從理論上說他們是不衝突的,也就是說可以鎖分離的。

如果使用獨佔鎖的話,則要求兩個操作在進行時首先要獲取當前佇列的鎖,那麼takeput就不是先真正的併發了,因此,在JDK中正是實現了兩種不同的鎖,一個是takeLock一個是putLock

5、鎖粗化

首先舉個簡單的例子:

public void syncMethod() {
        synchronized (lock) { //第一次加鎖
            method1();
        }
		method3();
        synchronized (lock) { //第二次加鎖
            mutextMethod();
        }
		method4();
        synchronized (lock) { //第三次加鎖
            method2();
        }
    }

  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

根據上述的程式碼,如果第一次和第二次加鎖和執行緒上下文切換的時間超過了method1()、method2()method3()、method4() 的時間,那麼我們倒不如使用下邊的方式:

public void syncMethod() {
        synchronized (lock) {
	        method1();
	        method3();
            mutextMethod();
            method4();
            method2();
        }
    }

  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

改進後的程式碼的執行時間可能小於上述分別加鎖的時間,這就是鎖粗化,也是一種鎖優化的方式,但是要根據具體的場景。

6、鎖消除

鎖消除是在編譯器級別的事情。

在即時編譯器時,如果發現不可能被共享的物件,則可以消除這些物件的鎖操作。

也許你會覺得奇怪,既然有些物件不可能被多執行緒訪問,那為什麼要加鎖呢?寫程式碼時直接不加鎖不就好了。

但是有時,這些鎖並不是程式設計師所寫的,有的是JDK實現中就有鎖的,比如VectorStringBuffer這樣的類,它們中的很多方法都是有鎖的。當我們在一些不會有執行緒安全的情況下使用這些類的方法時,達到某些條件時,編譯器會將鎖消除來提高效能。

7、Java虛擬機器對鎖的優化

可參考:Java多執行緒程式設計-(11)-從volatile和synchronized的底層實現原理看Java虛擬機器對鎖優化所做的努力

        </div>