一、優化動機
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);
}
}