線程安全的容器小結

線程安全的容器

列表

線程安全的列表有 Vector , CopyOnWriteArrayList 兩種,區別則主要在實現方式上,對鎖的優化上; 後者主要採用的是 copy-on-write 思路,修改時,拷貝一份出來,修改完成以後替換html

1. Vector 實現

vector 保證線程安全的原理比較簡單粗暴,直接在方法上加鎖java

get 方法

public synchronized E get(int index) {
   if (index >= elementCount)
       throw new ArrayIndexOutOfBoundsException(index);

   return elementData(index);
}

set 方法

public synchronized E set(int index, E element) {
   if (index >= elementCount)
       throw new ArrayIndexOutOfBoundsException(index);

   E oldValue = elementData(index);
   elementData[index] = element;
   return oldValue;
}

add方法

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

size方法

public synchronized int size() {
   return elementCount;
}

從上面幾個最最多見的幾個方法,就能夠看出,這個實現很是的簡單粗暴,所有上鎖,確定是線程安全的問題了;相應的問題也很明顯,效率妥妥的夠了,即使全是讀操做,都會有阻塞競爭,基本上徹底是無法忍的數組

2. CopyOnWriteArrayList 實現

使用了 copyOnWrite 機制,一句話,讀時直接讀,在修改時,先拷貝一份出來,在拷貝上進行修改,完成以後替換掉以前的引用安全

下面主要看一下幾個最多見的方法,是如何實現的,以此來研究下這套機制的玩法多線程

size 方法

public int size() {
   return getArray().length;
}

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

/**
* Gets the array.  Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
   return array;
}

對比一下 ArrayList 的獲取size方法,有一個size屬性記錄的是鏈表的長度,直接返回的這個值;而CopyOnWriteArrayList 則是每次都去實時查數組的長度併發

/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
    
public int size() {
        return size;
}

爲何這麼作 ?高併發

get方法

/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
   return get(getArray(), index);
}

private E get(Object[] a, int index) {
   return (E) a[index];
}
// ArrayList 
public E get(int index) {
   rangeCheck(index);

   return elementData(index);
}

E elementData(int index) {
   return (E) elementData[index];
}

和上面相同,一樣是先調用 getArray() 方法,而後在進行相應的操做,若是不這麼作,直接如 ArrayList 同樣的調用方式時(以下)性能

  • 假設數組長度爲 3, 如今獲取index=2(即最後一個)的元素值
  • rangeCheck 方法確認經過,elementData執行以前
  • 如今一個線程,刪除了一個元素,此時上面這個線程獲取時,就會出現數組越界

若是是上面的執行邏輯,則不會如此,由於操做的依然是老的那個數組對應的引用;當發生修改時,是在新的數組上進行的優化

add 方法

接下來則看一下具體的修改方法,是否是確實在新的數組上進行的操做,源碼以下:網站

public boolean add(E e) {
   final ReentrantLock lock = this.lock;
   lock.lock();
   try {
       Object[] elements = getArray();
       int len = elements.length;
       Object[] newElements = Arrays.copyOf(elements, len + 1);
       newElements[len] = e;
       setArray(newElements);
       return true;
   } finally {
       lock.unlock();
   }
}

關注幾點:

  • 上鎖: 這裏代表,每次修改都只能有一個線程在執行
  • 修改過程:
    • 拷貝原來內容到新的的數組上
    • 將元素添加在新的數組上
    • 更新列表中數組的引用,指向新的數組

set 方法

修改內容的值時,一樣是先加鎖,再修改,確保每次修改都是串行進行的;須要注意的一點是

  • 若設置的value和原來的內容相等,則不須要修改引用
  • 必定得確保釋放鎖
public E set(int index, E element) {
   final ReentrantLock lock = this.lock;
   lock.lock();
   try {
       Object[] elements = getArray();
       E oldValue = get(elements, index);

       if (oldValue != element) {
           int len = elements.length;
           Object[] newElements = Arrays.copyOf(elements, len);
           newElements[index] = element;
           setArray(newElements);
       } else {
           // Not quite a no-op; ensures volatile write semantics
           setArray(elements);
       }
       return oldValue;
   } finally {
       lock.unlock();
   }
}

小結

  1. Vector: 不管讀寫,所有加上了同步鎖,致使多線程訪問or修改時,鎖的競爭,效率較低

  2. CopyOnWriteArrayList : 讀不加鎖,在修改時,加鎖保證每次只有一個線程在修改列表;且修改的邏輯都是先拷貝一個副本出來,而後在副本上進行修改,最後在替換列表中數組的引用

  3. CopyOnWriteArraySet : 內部數組其實就是一個 CopyOnWriteArrayList, 相關方法也是直接來自 CopyOnWriteArrayList

Map

線程安全的Map則主要是 HashTable ConcurrentHashMap, 後者採用了分段鎖機制來提升併發訪問的性能

在便利時,操做Map的幾種場景分析

  1. 在遍歷時,修改Map的引用(即用一個新的map替換這個值)

    • 仍舊遍歷老的Map
  2. 在遍歷時,修改Map中的元素值

    • 會讀取到修改後的值
  3. 在遍歷時,新增or刪除元素

    • HashMap 會拋異常; ConcurrentHashMap 可正常執行

1. HashTable

同 Vector 同樣,經過對全部的方法添加 synchronized 關鍵字來確保的線程安全;缺點也很明顯,效率低,具體的幾個方法源碼以下,很少加說明了

public synchronized int size() {
   return count;
}

public synchronized Enumeration<K> keys() {
   return this.<K>getEnumeration(KEYS);
}

 public synchronized Enumeration<V> elements() {
   return this.<V>getEnumeration(VALUES);
}

public synchronized V get(Object key) {
   Entry<?,?> tab[] = table;
   int hash = key.hashCode();
   int index = (hash & 0x7FFFFFFF) % tab.length;
   for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
       if ((e.hash == hash) && e.key.equals(key)) {
           return (V)e.value;
       }
   }
   return null;
}


public synchronized V put(K key, V value) {
   // Make sure the value is not null
   if (value == null) {
       throw new NullPointerException();
   }

   // Makes sure the key is not already in the hashtable.
   Entry<?,?> tab[] = table;
   int hash = key.hashCode();
   int index = (hash & 0x7FFFFFFF) % tab.length;
   @SuppressWarnings("unchecked")
   Entry<K,V> entry = (Entry<K,V>)tab[index];
   for(; entry != null ; entry = entry.next) {
       if ((entry.hash == hash) && entry.key.equals(key)) {
           V old = entry.value;
           entry.value = value;
           return old;
       }
   }

   addEntry(hash, key, value, index);
   return null;
}

2. ConcurrentHashMap

一個ConcurrentHashMap由多個segment組成,每個segment都包含了一個HashEntry數組的hashtable, 每個segment包含了對本身的hashtable的操做,好比get,put,replace等操做,這些操做發生的時候,對本身的hashtable進行鎖定。因爲每個segment寫操做只鎖定本身的hashtable,因此可能存在多個線程同時寫的狀況,性能無疑好於只有一個hashtable鎖定的狀況

簡單來說,就是每一個 segment 的操做都是加鎖的;而多個 segment 的操做能夠是併發的

詳解能夠參考: Java集合---ConcurrentHashMap原理分析

更多能夠參考我的網站: 一灰的我的博客網站之Java之線程安全的容器

相關文章
相關標籤/搜索