同步技術新大陸--寫時複製技術(CopyOnWriteArrayList、CopyOnWriteArraySet)

一、寫時複製思想

寫入時複製是一種計算機程序設計領域的優化策略。其核心思想是,若是有多個調用者同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本)給該調用者,而其餘調用者所見到的最初的資源仍然保持不變。數組

二、集合中的寫時複製

集合數據主要就兩個操做讀、寫;讀是讀取內容,寫是改變內容;讀的時候讀取存儲資源,寫的時候,複製一份修改後再重置原有資源;這樣就具備如下特點:安全

  1. 讀數據、寫數據不會由於非原子操做,致使數據異常
  2. 寫數據間相互不影響
  3. 讀數據時,只是當時最新,並不必定是當時操做邏輯中最新
  4. 寫數據時,最後面寫數據會覆蓋前面寫數據

所以若是要作到線程安全,就必須再寫時進行加鎖bash

而CopyOnWriteArrayList集合就是這種寫時複製+寫時加鎖來實現的;CopyOnWriteArraySet內部代理了CopyOnWriteArrayList,進而實現不重複數據的集合;下面就來介紹下CopyOnWriteArrayList併發

三、CopyOnWriteArrayList集合

list集合,內部採用數組來存儲;寫時複製,並加鎖;直接讀數據;工具

final transient Object lock = new Object();
    private transient volatile Object[] array;
複製代碼

lock是對synchronized鎖標誌,array是資源數組,使用volatile關鍵字,寫操做會再任何線程下次操做資源時可見性能

3.1 寫數據

3.1.1 增長數據

public boolean add(E e) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }
    }
複製代碼
  1. synchronized關鍵字加鎖
  2. 獲取數組資源,並使用Arrays工具類copy數據
  3. 再copy數據中加入新數據
  4. 把copy數據從新寫回
public void add(int index, E element) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException(outOfBounds(index, len));
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        }
    }
複製代碼

特定索引增長數據,能夠任意位置增長數據;優化

  1. 校驗索引是否有效,無效拋出異常
  2. 分段copy數據,以index爲分割線(最開始、最後面一個,能夠一段copy完成)
  3. copy數據index位置加入數據
  4. 把copy數據從新寫回

保證數據不重複,增長數據ui

public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

    private boolean addIfAbsent(E e, Object[] snapshot) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i]
                        && Objects.equals(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }
    }
複製代碼
  1. 查找待增長的數據的索引
  2. 若是索引>= 0 代表存在,進一步處理,若是不存在則結束
  3. 加鎖重複檢查數據是否存在,存在,則結束
  4. 不存在,則copy數據,加入copy數據尾;並把copy數據寫回

3.1.2 改變數據

public E set(int index, E element) {
        synchronized (lock) {
            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 {
                setArray(elements);
            }
            return oldValue;
        }
    }
複製代碼

改變指定位置數據:spa

  1. 讀取指定位置index的數據
  2. 若是指定位置數據==要改變數據,則直接寫回;
  3. copy原數據,並置換index位置數據,並把copy數據寫回

3.1.3 刪除指定位置數據

public E remove(int index) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        }
    }
複製代碼

加鎖,進行copy數據,copy數據時,不包括要刪除的數據,把copy數據重置回去線程

3.1.4 刪除指定數據

public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    private boolean remove(Object o, Object[] snapshot, int index) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) findIndex: {
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i]
                        && Objects.equals(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);
            return true;
        }
    }
複製代碼
  1. 查找數據所在位置
  2. 未找到,則不處理
  3. 找到位置,則加鎖
  4. 從新檢查數據是否發生變換,若發生變化,則從新查找位置,並檢查是否有效,無效則退出
  5. 以刪除數據位置,分兩段進行copy數據,並把copy的數據重置回去

3.1.5 清除數據

public void clear() {
        synchronized (lock) {
            setArray(new Object[0]);
        }
    }
複製代碼

加鎖,置爲空數組

3.2 讀數據

很簡單的邏輯,獲取數據索引位置

public E get(int index) {
        return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
複製代碼

還有一些其它方法,好比獲取位置索引,集合大小等

三、CopyOnWriteArrayList集合

就不介紹了,它內部使用了CopyOnWriteArraySet來代理功能;而且減小了方法

四、總結

  1. 讀數據時不加鎖,這時讀的數據是可讀最新數據;這時多是讀資源時未寫回
  2. 不會發生數據錯誤問題
  3. 寫時加鎖+volatile保證,寫時數據順序執行,也即保證了同步
  4. 對位置等判斷,加鎖後須要從新判斷;這再寫低併發時,提升了性能
  5. 採用equal方法判斷相等

技術變化都很快,但基礎技術、理論知識永遠都是那些;做者但願在餘後的生活中,對經常使用技術點進行基礎知識分享;若是你以爲文章寫的不錯,請給與關注和點贊;若是文章存在錯誤,也請多多指教!

相關文章
相關標籤/搜索