淺談CopyOnWriteArrayList

java碼農之路 發佈 2023-06-12T01:02:29.455101+00:00

1.Copy-On-Write 是什麼?Copy-On-Write它是一種在計算機科學中常見的優化技術,主要應用於需要頻繁讀取但很少修改的數據結構上。

1.copy-On-Write 是什麼?

Copy-On-Write它是一種在計算機科學中常見的優化技術,主要應用於需要頻繁讀取但很少修改的數據結構上。

簡單的說就是在計算機中就是當你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之後呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了!

既然是一種優化策略,我們看一段代碼:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.iterator;
import java.util.List;

/**
* @author 百里
*/
public class BaiLiIteratorTest {
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> iter = list.iterator();
        while (iter.hasNext()) {
            System.err.println(iter.next());
        }
        System.err.println(Arrays.toString(list.toArray()));
    }
}

上面的Demo在單線程下執行時沒什麼毛病,但是在多線程的環境中,就可能出異常,為什麼呢?

因為多線程疊代時如果有其他線程對這個集合list進行增減元素,會拋出java.util.ConcurrentModificationException的異常。

我們以增加元素為例子,運行下面這Demo:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 並發疊代器問題示例代碼
 * @author 百里
 */
public class BaiLiConcurrentIteratorTest {
    // 創建一個ArrayList對象
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 給ArrayList添加三個元素:"1"、"2"和"3"
        list.add("1");
        list.add("2");
        list.add("3");

        // 開啟線程池,提交10個線程用於在list尾部添加5個元素"121"
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                for (int j = 0; j < 5; j++) {
                    list.add("121");
                }
            });
        }

        // 使用Iterator疊代器遍歷list並輸出元素值
        Iterator<String> iter = list.iterator();
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                while (iter.hasNext()) {
                    System.err.println(iter.next());
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        service.shutdown();
    }
}

這裡暴露的問題是什麼呢?

  • 多線程場景下疊代器遍歷集合的讀取操作和其他線程對集合進行寫入操作會導致出現並發修改異常

解決方案:

  • CopyOnWriteArrayList避免了多線程操作List線程不安全的問題

2.CopyOnWriteArrayList介紹

從JDK1.5開始Java並發包里提供了兩個使用CopyOnWrite機制實現的並發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的並發場景中使用到。

CopyOnWriteArrayList原理:

在寫操作(add、remove等)時,不直接對原數據進行修改,而是先將原數據複製一份,然後在新複製的數據上執行寫操作,最後將原數據引用指向新數據。這樣做的好處是讀操作(get、iterator等)可以不加鎖,因為讀取的數據始終是不變的。

接下來我們就看下源碼怎麼實現的。

3.CopyOnWriteArrayList簡單源碼解讀

add()方法源碼:

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#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);//將引用指向新數組  1
        return true;
    } finally {
        lock.unlock();//解鎖啦
    }
}

可以看到,CopyOnWriteArrayList中的寫操作都需要先獲取鎖,然後再將當前的元素數組複製一份,並在新複製的元素數組上執行寫操作,最後將數組引用指向新數組。

@SuppressWarnings("unchecked")
public E next() {
    if (! hasNext()) //是否存在下一個元素
        throw new NoSuchElementException(); //沒有下一個元素,則會拋出NoSuchElementException異常
    //snapshot是一個類成員變量,它是在創建疊代器時通過複製集合內容而獲得的一個數組。
    //cursor是另一個類成員變量,初始值為0,並在每次調用next()時自增1,表示當前返回元素的位置。
    return (E) snapshot[cursor++];
}

而讀操作不需要加鎖,直接返回當前的元素數組即可。

這種寫時複製的機制保證了讀操作的線程安全性,但是會犧牲一些寫操作的性能,因為每次修改都需要複製一份數組。因此,適合讀遠多於寫的場合。

所以我們將多線程Demo中的ArrayList改為CopyOnWriteArrayList,執行就不會報錯啦!

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 並發疊代器問題示例代碼
* @author 百里
*/
public class BaiLiConcurrentIteratorTest {
    // 創建一個ArrayList對象
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 給ArrayList添加三個元素:"1"、"2"和"3"
        list.add("1");
        list.add("2");
        list.add("3");

        // 開啟線程池,提交10個線程用於在list尾部添加5個元素"121"
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                for (int j = 0; j < 5; j++) {
                    list.add("121");
                }
            });
        }

        // 使用Iterator疊代器遍歷list並輸出元素值
        Iterator<String> iter = list.iterator();
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                while (iter.hasNext()) {
                    System.err.println(iter.next());
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        service.shutdown();
    }
}

4.CopyOnWriteArrayList優缺點

優點:

  1. 線程安全。CopyOnWriteArrayList是線程安全的,由於寫操作對原數據進行複製,因此寫操作不會影響讀操作,讀操作可以不加鎖,降低了並發衝突的概率。
  2. 不會拋出ConcurrentModificationException異常。由於讀操作遍歷的是不變的數組副本,因此不會拋出ConcurrentModificationException異常。

缺點:

  1. 寫操作性能較低。由於每一次寫操作都需要將元素複製一份,因此寫操作的性能較低。
  2. 內存占用增加。由於每次寫操作都需要創建一個新的數組副本,因此內存占用會增加,特別是當集合中有大量數據時,內存占用較高。
  3. 數據一致性問題。由於讀操作遍歷的是不變的數組副本,因此在對數組執行寫操作期間,讀操作可能讀取到舊的數組數據,這就涉及到數據一致性問題。

5.CopyOnWriteArrayList使用場景

  • 讀多寫少。為什麼?因為寫的時候會複製新集合
  • 集合不大。為什麼?因為寫的時候會複製新集合
  • 實時性要求不高。為什麼,因為有可能會讀取到舊的集合數據
關鍵字: