源碼閱讀(29):Java中線程安全的List結構——CopyOnWriteArrayList(2)

(接上文《源碼閱讀(28):Java中線程安全的List結構——CopyOnWriteArrayList(1)》)

4、CopyOnWriteArrayList的主要方法

當完成CopyOnWriteArrayList集合的初始化過程的介紹後,本文再列舉幾個該集合典型的方法,以便幫助讀者理解該集合是如果基於一個內存副本完成寫操作的,以及這樣做的有點和缺點。

4.1、get(int)方法

get(int)方法是從CopyOnWriteArrayList集合中獲取指定索引位置上元素對象的方法,該方法無需保證線程安全性,任務操作者、任何線程、任何時間點都可以通過該方法或類似方法獲取CopyOnWriteArrayList集合中的數據,究其根本原因,就是因爲該集合的所有寫操作都是在一個內存副本中進行,所以任何讀性質的操作都不會受影響

public E get(int index) {
  return get(getArray(), index);
}

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

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
  return (E) a[index];
}

代碼很簡單,以至於無非進行任何說明。這種數據讀取方式因爲不需要考慮任何鎖機制,並且數組可以支持隨機位置上的讀操作,所以其時間複雜度任何時候都爲O(1)。

4.2、add(E)方法

使用add方法,向CopyOnWriteArrayList集合的最後一個數組索引位添加一個新的元素(引用),添加的元素可以爲null。源代碼片段如下所示:

public boolean add(E e) {
  final ReentrantLock lock = this.lock;
  // 通過lock所對應獲取操作以下代碼的操作權
  lock.lock();
  try {
    // 獲取到當前集合使用的數組對象
    Object[] elements = getArray();
    // 獲取當前集合的元素大小
    int len = elements.length;
    // 使用Arrays.copy方法創建一個內存副本newElements數組
    // 注意,副本數組的容量比當前CopyOnWriteArrayList集合的容量大1
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    // 在newElements數組的最後一個索引位新增這個元素(引用)
    newElements[len] = e;
    // 設置完成後,最後將當前使用數組替換成副本,是副本數組成爲CopyOnWriteArrayList集合的內部數組
    setArray(newElements);
    return true;
  } finally {
    // 最後釋放操作權
    lock.unlock();
  }
}

這裏需要注意一個細節:在add方法所有處理邏輯開始前,先進行CopyOnWriteArrayList集合的操作權獲取,它並不影響CopyOnWriteArrayList集合的讀操作,因爲通過上一小節中介紹get方法的源代碼內容可知,CopyOnWriteArrayList集合的讀操作完全無視鎖權限,也不會有多線程下的數據操作問題。之所以類似add方法這樣的CopyOnWriteArrayList容器寫操作方法需要獲取操作權,主要是爲了防止其它線程可能對CopyOnWriteArrayList集合同時進行的寫操作造成數據錯誤

從以上add方法的詳細描述我們可以知道,該集合通過Arrays.copyOf方法(其內部是System.arraycopy方法)創建一個新的內存區域,存放數組副本,並在副本上進行寫操作,最後將CopyOnWriteArrayList集合中的數組引用爲副本數組。
在這裏插入圖片描述

4.3、set(int , E)方法

set方法用於替換CopyOnWriteArrayList集合指定數組索引位上的元素。該方法的操作過程和add方法類似,源代碼如下所示:

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);
    }
    // 如果原始對象和將要重新設定的對象相等(依據內存地址)
    // 從理論上講,無需對CopyOnWriteArrayList集合的當前數組重新進行設定,
    // 但這裏還是重新設定了一次
    else {
      // Not quite a no-op; ensures volatile write semantics
      setArray(elements);
    }
    // 處理完成後,該索引爲上原始的數據對象將會被返回
    return oldValue;
  } finally {
    lock.unlock();
  }
}

請注意源代碼中的一句註釋:“Not quite a no-op; ensures volatile write semantics”,當指定數組索引位上的原始數據對象和將要新替換的數據對象“相等”時,從理論上講實際上就不需要創建副本進行寫操作,也不再需要通過setArray方法進行數組替換操作了。

但從以上源代碼中我們可以看到當以上場景出現時,源代碼仍然調用了setArray方法進行數組的設置操作,爲什麼會這樣呢?這主要是爲了保證外部調用者的非volatile變量遵循happen−before原則。該原則涉及到JMM(java內存模型)和指令重排的知識點,有興趣的讀者可自行查詢資料。

5、Collections.synchronizedList的補充作用

5.1、CopyOnWriteArrayList工作機制的優缺點

從以上關於CopyOnWriteArrayList集合的工作介紹中,我們可以大致歸納出CopyOnWriteArrayList集合的特點和它的一些優缺點:

  • 該集合適合應用在多線程並行場景下,如果讀者使用集合的場景中不涉及多線程操作,則不建議使用該集合。甚至不建議使用java.util.concurrent包下的任何集合類——使用java.util包中的基本java集合框架即可。

  • 該集合在多線程併發操作的場景下,主要的關注點集中在於如何保證集合的線程安全性,和集合的數據讀操作性能。爲此,該集合以顯著犧牲自身的寫操作性能和內存空間的方式,來換取讀操作性能不受影響。這個特徵很好理解,每次進行讀操作前都要創建一個內存副本,這種操作一定會對內存空間造成浪費,且內存複製操作一定會造成多餘的性能消耗。

  • 所以這種集合適用於多線程併發操作場景下,那些多線程讀操作遠遠大於寫操作次數,且集合中存儲的數據規模不大的場景。

5.2、Collections.synchronizedList對CopyOnWriteArrayList的補充

那麼Java原生類、工具包中,有沒有提供一些適合在多線程併發操作場景下使用的,其讀操作性能和寫操作性能保持一定平衡性的,雖然整體性能不是最好但依然保證線程安全的,最後又是List性質的集合呢?答案是:有的。

java.util.Collections是Java爲開發人員提供的一個和集合操作相關的工具包(JDK1.2便開始提供,各版本又做了不同程度的代碼調整),其中提供了一組方法,可以將java.util包下的那些線程不安全的集合轉變爲線程安全的集合。實際上就是使用java object monitor機制,將集合方法進行了封裝。請看如下示例:

// ......
// 通過Collections提供的synchronizedList方法
// 將線程不安全的ArrayList封裝爲線程安全的List集合
List<String> syncList = java.util.Collections.synchronizedList(new ArrayList<>());
// 對於使用者來說,集合的增刪改查功能不受影響
syncList.add("a");
syncList.add("b");
syncList.add("c");
syncList.add("d");
syncList.add("e");
// ......

// 通過Collections提供的synchronizedList方法
// 將線程不安全的TreeSet集合封裝爲線程安全的Set集合
Set<String> syncSet = java.util.Collections.synchronizedSortedSet(new TreeSet<>());
syncSet.add("a");
syncSet.add("b");
syncSet.add("c");
syncSet.add("d");
syncSet.add("e");
// ......

但是,使用經過這個工具封裝的集合需要特別注意一點,就是原始集合的迭代器(iterator)、可拆分的迭代器(spliterator)、處理流(stream)、並行流(parallelStream)的運行都不受線程安全的封裝保護,如果用戶需要這樣的集合使用方式,則必須自行控制線程安全。

另外,由於java.util.Collections.synchronizedXXXX這樣的線程安全集合封裝方式,其內部使用的是java object monitor這種鎖機制,所以它也不適合在併發量非常高的場景中使用。最後我們基於java.util.Collections.synchronizedList方法,簡述一下其內部的工作原理:

public class Collections {
  // Suppresses default constructor, ensuring non-instantiability.
  private Collections() {
  }
  
  // ......
  // 其中包括了Collection接口中各方法的實現
  static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    // Backing Collection
    // 被封裝的真實集合,由該屬性記錄(引用)
    final Collection<E> c;
    // Object on which to synchronize
    // 整個集合線程安全性封裝的機制中,使用該對象管理Object Monitor鎖機制
    final Object mutex;
    // ......
    // 以下諸如size、add這樣的集合操作方法,全部基於mutex對象,基於Objct Monitor進行封裝
    public int size() {
      synchronized (mutex) {return c.size();}
    }
    public boolean add(E e) {
      synchronized (mutex) {return c.add(e);}
    }
    // ......
    // 以下這些方法沒有進行線程安全封裝,需要使用者手動控制
    // Must be manually synched by user!
    public Iterator<E> iterator() {
      return c.iterator(); 
    }
    // Must be manually synched by user!
    @Override
    public Spliterator<E> spliterator() {
      return c.spliterator(); 
    }
    // Must be manually synched by user!
    @Override
    public Stream<E> stream() {
        return c.stream(); 
    }
    // Must be manually synched by user!
    @Override
    public Stream<E> parallelStream() {
        return c.parallelStream(); 
    }
  }
  
  // ......
  
  // 該類繼承於SynchronizedCollection
  static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    // 被封裝的真實List集合,由該屬性記錄(引用)
    final List<E> list;
	// 並且通過構造函數的重寫,將父類中c屬性賦值爲當前的list屬性
    SynchronizedList(List<E> list) {
      super(list);
      this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
      super(list, mutex);
      this.list = list;
    }
    // ......
    // 諸如以下由list接口定義的操作方法,也被重新封裝
    public E get(int index) {
      synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
      synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
      synchronized (mutex) {list.add(index, element);}
    }
  }

  // ......
  
  // 該類繼承於SynchronizedList,當封裝的List性質的集合支持RandomAccess隨機訪問
  // 就使用該類進行線程安全性封裝
  static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
    public List<E> subList(int fromIndex, int toIndex) {
      synchronized (mutex) {
        return new SynchronizedRandomAccessList<>(list.subList(fromIndex, toIndex), mutex);
      }
    }
  }
}

以上源代碼展示了SynchronizedRandomAccessList、SynchronizedList和SynchronizedCollection這三個工具類的繼承關係,以及它們三者是如何配合完成集合線程安全性封裝控制的,如下圖所示:

在這裏插入圖片描述

現在我們來看一下當調用java.util.Collections.synchronizedList方法時發生了什麼事情:

public static <T> List<T> synchronizedList(List<T> list) {
  // 如果當前List集合支持RandomAccess隨機訪問,則使用SynchronizedRandomAccessList對集合進行線程安全性封裝
  // 否則使用SynchronizedList對集合進行線程安全性封裝
  return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list));
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章