天天看點

分段鎖的原理

前言:在分析ConcurrentHashMap的源碼的時候,了解到這個并發容器類的加鎖機制是基于粒度更小的分段鎖,分段鎖也是提升多并發程式性能的重要手段之一。

在并發程式中,串行操作是會降低可伸縮性,并且上下文切換也會減低性能。在鎖上發生競争時将通水導緻這兩種問題,使用獨占鎖時保護受限資源的時候,基本上是采用串行方式—-每次隻能有一個線程能通路它。是以對于可伸縮性來說最大的威脅就是獨占鎖。

我們一般有三種方式降低鎖的競争程度:

1、減少鎖的持有時間

2、降低鎖的請求頻率

3、使用帶有協調機制的獨占鎖,這些機制允許更高的并發性。

在某些情況下我們可以将鎖分解技術進一步擴充為一組獨立對象上的鎖進行分解,這成為分段鎖。其實說的簡單一點就是:容器裡有多把鎖,每一把鎖用于鎖容器其中一部分資料,那麼當多線程通路容器裡不同資料段的資料時,線程間就不會存在鎖競争,進而可以有效的提高并發通路效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先将資料分成一段一段的存儲,然後給每一段資料配一把鎖,當一個線程占用鎖通路其中一個段資料的時候,其他段的資料也能被其他線程通路。

比如:在ConcurrentHashMap中使用了一個包含16個鎖的數組,每個鎖保護所有散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護。假設使用合理的雜湊演算法使關鍵字能夠均勻的分部,那麼這大約能使對鎖的請求減少到越來的1/16。也正是這項技術使得ConcurrentHashMap支援多達16個并發的寫入線程。

當然,任何技術必有其劣勢,與獨占鎖相比,維護多個鎖來實作獨占通路将更加困難而且開銷更加大。

下面給出一個基于散列的Map的實作,使用分段鎖技術。

import java.util.Map;

/**
 * Created by louyuting on 17/1/10.
 */
public class StripedMap {
  //同步政策: buckets[n]由 locks[n%N_LOCKS] 來保護

  private static final int N_LOCKS = 16;//分段鎖的個數
  private final Node[] buckets;
  private final Object[] locks;

  /**
   * 結點
   * @param <K>
   * @param <V>
   */
  private static class Node<K,V> implements Map.Entry<K,V>{
    final K key;//key
    V value;//value
    Node<K,V> next;//指向下一個結點的指針
    int hash;//hash值

    //構造器,傳入Entry的四個屬性
    Node(int h, K k, V v, Node<K,V> n) {
      value = v;
      next = n;//該Entry的後繼
      key = k;
      hash = h;
    }

    public final K getKey() {
      return key;
    }

    public final V getValue() {
      return value;
    }

    public final V setValue(V newValue) {
      V oldValue = value;
      value = newValue;
      return oldValue;
    }

  }

  /**
   * 構造器: 初始化散列桶和分段鎖數組
   * @param numBuckets
   */
  public StripedMap(int numBuckets) {
    buckets = new Node[numBuckets];
    locks = new Object[N_LOCKS];

    for(int i=0; i<N_LOCKS; i++){
      locks[i] = new Object();
    }

  }

  /**
   * 傳回散列之後在散列桶之中的定位
   * @param key
   * @return
   */
  private final int hash(Object key){
    return Math.abs(key.hashCode() % N_LOCKS);
  }


  /**
   * 分段鎖實作的get
   * @param key
   * @return
   */
  public Object get(Object key){
    int hash = hash(key);//計算hash值

    //擷取分段鎖中的某一把鎖
    synchronized (locks[hash% N_LOCKS]){
      for(Node m=buckets[hash]; m!=null; m=m.next){
        if(m.key.equals(key)){
          return m.value;
        }
      }
    }

    return null;
  }

  /**
   * 清除整個map
   */
  public void clear() {
    //分段擷取散列桶中每個桶地鎖,然後清除對應的桶的鎖
    for(int i=0; i<buckets.length; i++){
      synchronized (locks[i%N_LOCKS]){
        buckets[i] = null;
      }
    }
  }
}           

複制

上面的實作中:使用了N_LOCKS個鎖對象數組,并且每個鎖保護容器的一個子集,對于大多數的方法隻需要回去key值的hash散列之後對應的資料區域的一把鎖就行了。但是對于某些方法卻要獲得全部的鎖,比如clear()方法,但是獲得全部的鎖不必是同時獲得,可以使分段獲得,具體的檢視源碼。

這就是分段鎖的思想。

本文來源于網絡,主要摘自:

https://blog.csdn.net/u010853261/article/details/54314486