天天看點

Java鎖優化Java鎖優化

Java鎖優化

應用程式在并發環境下會産生很多問題,通常情況下,我們可以通過加鎖來解決多線程對臨界資源的通路問題。但是加鎖往往會成為系統的瓶頸,因為加鎖和釋放鎖會涉及到與作業系統的互動,會有很大的性能問題。那麼這個時候基于鎖的優化手段就顯得很重要了。

一般情況下,可以從兩個角度進行鎖優化:對單個鎖算法的優化和對鎖粒度的細分。

1. 單個鎖的優化

自旋鎖:

​ 非自旋鎖在未擷取鎖的情況會被阻塞,之後再喚醒嘗試獲得鎖。而JDK的阻塞和喚醒是基于作業系統實作的,會有系統資源的開銷。自旋鎖就是線程不停地循環嘗試獲得鎖,而不會将自己阻塞,這樣不會浪費系統的資源開銷,但是會浪費CPU的資源。所有現在的JDK大部分都是先自旋等待,如果自旋等待一段時間之後還沒有擷取到鎖,就會将目前線程阻塞。

鎖消除:

​ 當JVM分析代碼時發現某個方法隻被單個線程安全通路,而且這個方法是同步方法,那麼JVM就會去掉這個方法的鎖。

單個鎖優化的瓶頸:

​ 對單個鎖優化的效果就像提高單個CPU的處理能力一樣,最終會由于各個方面的限制而達到一個平衡點,到達這個點之後優化單個鎖的對高并發下面鎖的優化效果越來越低。是以将一個鎖進行粒度細分帶來的效果會很明顯,如果一個鎖保護的代碼塊被拆分成兩個鎖來保護,那麼程式的效率就大約能夠提高到2倍,這個比單個鎖的優化帶來的效果要明顯很多。常見的鎖粒度細分技術有:鎖分解和鎖分段

2. 細分鎖粒度

細分鎖粒度的目的是降低競争鎖的機率。

2.1 鎖分解

鎖分解的核心是将無關的代碼塊,如果在一個方法中有一部分的代碼與鎖無關,一部分的代碼與鎖有關,那麼可以縮小這個鎖的傳回,這樣鎖操作的代碼塊就會減少,鎖競争的可能性也會減少

縮小鎖的範圍

縮小鎖的範圍是指盡量隻在必要的地方加鎖,不要擴大加鎖的範圍,就拿單例模式舉例,範圍大的鎖可能将整個方法都加鎖了:

class Singleton {
  private Singleton instance;

  private Singleton() {
  }

  // 将整個方法加鎖
  public synchronized Singleton getInstance() {
    try {
      Thread.sleep(1000);  //do something
      if(null == instance)
        instance = new Singleton();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    return instance;
  }

}           

優化後的,隻将部分代碼加鎖:

class Singleton {
  private Singleton instance;

  private Singleton() {
  }

  public Singleton getInstance() {
    try {
      Thread.sleep(1000);  //do something
      // 隻對部分代碼加鎖
      synchronized(this) {
        if(null == instance)
          instance = new Singleton();
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    return instance;
  }
}           

減少鎖的粒度

減少鎖的粒度是指如果一個鎖需要保護多個互相獨立的變量,那麼可以将一個鎖分解為多個鎖,并且每個鎖保護一個變量,這樣就可以減少鎖沖突。看一下下面的例子:

class Demo{
  private Set<String> allUsers = new HashSet<String>();
  private Set<String> allComputers = new HashSet<String>();

  //公用一把鎖
  public synchronized void addUser(String user){ 
    allUsers.add(user);
  }
  
  public synchronized void addComputer(String computer){
    allComputers.add(computer);
  }
}           

縮小鎖的粒度後,将一個鎖拆分為多個:

class Demo{
  private Set<String> allUsers = new HashSet<String>();
  private Set<String> allComputers = new HashSet<String>();
  
  //分解為兩把鎖
  public void addUser(String user){ 
    synchronized (allUsers){
      allUsers.add(user);
    }
  }
  
  public void addComputer(String computer){
    synchronized (allComputers){
      allComputers.add(computer);
    }
  }
}           

如上的方法把一個鎖分解為2個鎖時候,采用兩個線程時候,大約能夠使程式的效率提升一倍。

2.2 鎖分段

鎖分段和縮小鎖的粒度類似,就是将鎖細分的粒度更多,比如将一個數組的每個位置當做單獨的鎖。JDK8以前ConcurrentHashMap就使用了鎖分段技術,它将散列數組分成多個Segment,每個Segment存儲了實際的資料,通路資料的時候隻需要對資料所在的Segment加鎖就行。