併發-7-同步容器和ConcurrentHashMap

Coding挖掘機發表於2018-10-17

同步容器是什麼:

JDK提供給了很多容器,其中有list,set,queue,map等。

這裡我們挑出List單講。

眾所周知,很多書上,我們看到Arraylist並不是執行緒安全的,Vector是執行緒安全的。

那就從原始碼上分析一下:

ArrayList中,add方法如下:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
複製程式碼

Vector中,add方法如下:

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
複製程式碼

對比發現,Vector之所以是執行緒安全的,是因為Vector對所有的方法使用synchronized進行了修飾。

不安全的同步容器:

public class SynchornizedVector {

    public static void main(String[] agrs){
        Vector vector = new Vector();
        for(int i =0 ; i<10; i++){
            vector.add(i,i);
        }

        new Thread(){
            @Override
            public void run() {
                //vector共有10個元素,index對應0-9
                //第一步:執行緒1執行到j=8,暫停;
                for(int j = 0; j < vector.size(); j++){
                    //第三部,執行緒1繼續執行,要獲取vector.get(8)的時候出錯,因為vector的元素已經被執行緒2清空
                    if(j == 8){
                        try {
                            Thread.currentThread().sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(vector.get(j));
                }
            }
        }.start();

        new Thread(){

            @Override
            public void run() {
                //第二步:執行緒2獲得時間片,立即執行,刪除掉vector中所有的元素
                for(int i = 0; i < vector.size(); i++){
                    vector.remove(i);
                }
            }
        }.start();
    }
}
複製程式碼

需要對size()的地方進行同步互斥,才能確保容器是安全的,舉例如下:

第39行和第17行

public class SynchornizedVector {

    public static void main(String[] agrs) {
        Vector vector = new Vector();

        for (int i = 0; i < 10; i++) {
            vector.add(i, i);
        }


        new Thread() {
            @Override
            public void run() {
                //vector共有10個元素,index對應0-9
                //第一步:執行緒1執行到j=8,暫停;

                synchronized (SynchornizedVector.class) {
                    for (int j = 0; j < vector.size(); j++) {
                        //第三部,執行緒1繼續執行,要獲取vector.get(8)的時候出錯,因為vector的元素已經被執行緒2清空
                        if (j == 8) {
                            try {
                                Thread.currentThread().sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(vector.get(j));
                    }
                }
            }
        }.start();

        new Thread() {

            @Override
            public void run() {
                //第二步:執行緒2獲得時間片,立即執行,刪除掉vector中所有的元素

                synchronized (SynchornizedVector.class) {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }

            }
        }.start();
    }
}
複製程式碼

工程中大量使用的同步容器ConcurrentHashMap

  眾所周知,hashMap是根據雜湊值分段儲存的,同步Map在同步的時候鎖住了所有的段(粗粒度的鎖)

  而ConcurrentHashMap根據雜湊值鎖定了雜湊值對應的段,提高了併發效能(細粒度的鎖)

  其資料結構如下:

併發-7-同步容器和ConcurrentHashMap
  根據圖中的資料結構:

  每次對key尋找到相應的位置需要兩次定位:1.定位到Segment。2.定位到元素所在Segment中的具體連結串列的頭部。

  對讀操作不加鎖,對寫操作的鎖的粒度細化到每個Segment

  支援的最大併發數就是Segment的數量
  

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile int count;
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry<K,V>[] table;
    final float loadFactor;
}
複製程式碼

count:Segment中元素的數量

modCount:對table的大小造成影響的操作的數量,比如put(),remove()

threshold:擴容閾值

table:陣列中每一個元素代表了一個連結串列的頭部

loadFactor:用於確定threshold

get過程

static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
}
複製程式碼
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
  
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
  
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    segmentShift = 32 - sshift;
    segmentMask = ssize - 1;
    this.segments = Segment.newArray(ssize);
  
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = 1;
    while (cap < c)
        cap <<= 1;
  
    for (int i = 0; i < this.segments.length; ++i)
        this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
複製程式碼

initialCapcity:初始的容量

loadFactor:負載引數

concurrentLevel:Segment的數量,一旦設定不可改變,如果map容量不夠,需要擴容,則增加Segment陣列的大小,而不增加Segment的數量,這樣就不需要對Map做rehash,只要對Segment中的元素做rehash

整個ConcurrentHashMap的初始化方法還是非常簡單的,先是根據concurrentLevel來new出Segment,這裡Segment的數量是不大於concurrentLevel的最大的2的指數,就是說Segment的數量永遠是2的指數個,這樣的好處是方便採用移位操作來進行hash,加快hash的過程。接下來就是根據intialCapacity確定Segment的容量的大小,每一個Segment的容量大小也是2的指數,同樣使為了加快hash的過程。

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}
複製程式碼

第三行的作用是:把key對應的segment找出來

final Segment<K,V> segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}
複製程式碼

採用移位的方式操作,可以加快計算速度

確定了具體的segment之後,就要確定segment中具體的連結串列位置

HashEntry<K,V> getFirst(int hash) {
    HashEntry<K,V>[] tab = table;
    return tab[hash & (tab.length - 1)];
}
複製程式碼
V get(Object key, int hash) {
    if (count != 0) { // read-volatile
        HashEntry<K,V> e = getFirst(hash);
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value;
                if (v != null)
                    return v;
                return readValueUnderLock(e); // recheck
            }
            e = e.next;
        }
    }
    return null;
}
複製程式碼

put過程:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();
    try {
        int c = count;
        if (c++ > threshold) // ensure capacity
            rehash();
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;
  
        V oldValue;
        if (e != null) {
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        }
        else {
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K,V>(key, hash, first, value);
            count = c; // write-volatile
        }
        return oldValue;
    } finally {
        unlock();
    }
}
複製程式碼

如果Segment中元素的數量超過了threshold就要進行rehash,如有key存在,則更新value值,否則新生成一個HashEntry加入到整個Segment的頭部

注意:

ConcurrentHashMap 的 get 的操作在大多數情況下都是不加鎖的,只有當找到的 HashEntry 的 value 是 null 時,才會再進行一次加鎖的讀操作,以保障讀操作的一致性。通常這種情況發生在你找到的 HashEntry 恰是另一個執行緒在做 put 操作時建立的,且 value 恰好沒有設定完成。這種情況不太容易發生。所以,對於 ConcurrentHashMap 來說,發生在同一個 Segment 的一個寫和多個讀操作是並不互斥的,所以 Segment 也就沒有繼承讀寫鎖了,而且這種設計要比讀寫鎖的併發能力更高

相關文章