關於 CopyOnWriteArrayList 的一個簡單優化

一、優化動機

COW 簡介:增刪改都會加鎖並拷貝工作數組,在拷貝數組上做完增刪改操作後,會把拷貝數組切換爲工作數組,在這個過程中,不會阻塞讀操作。

所以 COW 很適合於讀多寫少的情況。

但是如果寫再多一點呢,或者大部分的增刪改只發生在工作數組的末尾呢。對於這些情況,在有大量數據的業務場景中,每次寫操作都要拷貝整個工作數組,是很不划算的。

二、優化實現

考慮大部分的增刪改只發生在工作數組末尾的這種情況。

我們可以使用一個數組來保存初始數據,然後後續發生在數組末尾的增刪改數據保存在另一個數組中。

這樣對於大量的增刪改操作就只需要拷貝後面那個較短的數組即可,而不需要拷貝整個數據。

所以我考慮使用兩個數組來保存數據,兩個數組在邏輯上當成一個數組的前後兩段使用。

三、優化導致的問題和解決方案

3.1 讀的效率問題

COW 最顯著的優點,就是讀很快,任何寫操作都不會阻塞讀操作。所以對 COW 的優化,也應該具有極高的讀效率。

兩個數組當成一個數組使用的這個優化,對讀操作最明顯的影響是,在每次讀的時候,都需要判斷讀操作發生在兩個數組中的哪一個上面。

對於這個問題,每次通過判斷語句判斷,顯然是很影響效率的。

對應的解決辦法是,利用 Java 底層對數組訪問做的安全控制,我們在代碼層面不加以判斷,等底層判斷髮現越界並拋出數組越界異常之後,再去處理。
代碼示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 爲第一段數組
		} catch (ArrayIndexOutOfBoundsException e) {
			return c[index-o.length]; // c 爲第二段數組
		}
	}

可以看到,這樣做,對於發生在比較長的第一段數組中的讀完全沒有任何影響。但對於讀第二段較短的數組,由於需要捕獲數組越界異常,並做一個減法下標映射,所以會稍微影響讀效率。

另外,還有一個小問題是,最終拋出的越界異常是針對第二段數組的,給出的越界信息會不正確。不過這個小問題不關乎大局,完全可以忽略。
糾正數組越界信息,代碼示意如下:

	public E get(int index) {
		try{
			return o[index]; // o 爲第一段數組
		} catch (ArrayIndexOutOfBoundsException e) {
			try{
				return c[index-o.length]; // c 爲第二段數組
			} catch (ArrayIndexOutOfBoundsException e1) {
				// 拋出正確的越界信息
				throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

3.2 讀的正確性問題

上一個問題的解決方案,又會引發出另一個問題。
即,在讀第二段數組中的元素時,第一段數組的長度不能變長。

舉例說明,假設第一段數組長度爲 10,第二段數組長度爲 5。
此時用戶想要 get(10),讀第一段數組時,發現越界轉而去讀第二段數組。
此時另一個用戶在第一段數組中添加元素,並且已經執行完拷貝、增加、切換數組的操作。那麼此時第一段數組的長度就變成了 11。前一個用戶的 get(10) 執行到 c[index-o.length] 時,index-o.length 爲 -1,此時變成想要訪問第二段數組下標爲 -1 的元素,顯然會拋出原本不應該拋出的數組越界異常。

解決方案一:
不允許在第一段數組上增加元素。

解決方案二:
捕獲發生在第二段數組上的數組越界異常。
如果發現是第一段數組變長所致,嘗試重新讀。有點自旋鎖的意思。
代碼示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 爲第一段數組
		} catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c[i = index-o.length]; // c 爲第二段數組
			} catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0) return get(index); // 重新讀
				else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

對於讀多寫少的場景,完全可以這樣遞歸讀。

如果寫操作比較頻繁,也可以對第一段數組加鎖,然後再次進行讀操作。
代碼示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 爲第一段數組
		} catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c[i = index-o.length]; // c 爲第二段數組
			} catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0){
					synchronized (o) { // 此時,讀操作有一定概率被阻塞
						return get(index);
					}
				}else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

這樣,大多數的讀,都和之前一樣。只有在讀第二段數組時,第一段數組變長,並且 index-o.length 爲負這種情況,纔會阻塞讀操作。

四、示意代碼

時間關係,只實現了構造方法和基本的增刪改查。

package main;

import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;

public class IncrementCOW<E> {

	// 成員變量
	private volatile Object[] o;
	private CopyOnWriteArrayList<E> c = new CopyOnWriteArrayList<>();

	// 構造方法
	public IncrementCOW(){
		o = new Object[0];
	}
	
	public IncrementCOW(E[] toCopyIn){
		o = Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class);
	}

	public IncrementCOW(Collection<? extends E> c){
		Object[] elements = c.toArray();
		// c.toArray might (incorrectly) not return Object[] (see 6260652)
		if (elements.getClass() != Object[].class)
			elements = Arrays.copyOf(elements, elements.length, Object[].class);
		o = elements;
	}

	// 普通方法
	public int size(){
		return o.length + c.size();
	}

	@SuppressWarnings("unchecked")
	private E get(Object[] a, int index) {
		return (E) a[index];
	}
	
	public E get(int index) {
		try{
			return get(o, index);
		}catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c.get(i = index-o.length);
			}catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0) return get(index); // 不加鎖的方式
				else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

	/**
	 * 沒有同時給 o 和 c 加鎖
	 * 所以在 set c 的時候,index 可能不是邏輯數組實時的下標
	 * 只保證 index 是用戶調用 set 方法後邏輯數組一個快照的下標
	 * */
	public E set(int index, E element) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				E oldValue = get(o, index);
				if(oldValue != element){
					Object[] newElements = Arrays.copyOf(o, o.length);
                	newElements[index] = element;
                	o = newElements;
				}
                return oldValue;
			}
		}
		return c.set(i, element);
	}

	public void add(E e) {
		c.add(e);
	}

	// 同 set 方法,沒有同時給 o 和 c 加鎖
	public void add(int index, E element) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				Object[] newElements = new Object[o.length + 1];
				System.arraycopy(o, 0, newElements, 0, index);
				System.arraycopy(o, index, newElements, index + 1,
						-i);
				newElements[index] = element;
				o = newElements;
			}
		}
		c.add(i, element);
	}

	// 同 set 方法,沒有同時給 o 和 c 加鎖
	public E remove(int index) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				E oldValue = get(o, index);
				int numMoved = -i - 1;
	            if(numMoved == 0)
	                o = Arrays.copyOf(o, o.length - 1);
	            else{
	                Object[] newElements = new Object[o.length - 1];
	                System.arraycopy(o, 0, newElements, 0, index);
	                System.arraycopy(o, index + 1, newElements, index,
	                                 numMoved);
	                o = newElements;
	            }
	            return oldValue;
			}
		}
		return c.remove(i);
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章