最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java Arraylist在多線程環(huán)境下的問題與解決方案

 更新時間:2026年03月19日 09:22:58   作者:加洛斯  
這篇文章主要介紹了關(guān)于arraylist的多線程問題以及多種場景下的解決方案,同時詳細簡述了其核心內(nèi)容和原理,希望對大家有一定的幫助

一、ArrayList 的線程不安全性

ArrayList 的所有方法都沒有進行同步控制,多個線程同時添加、刪除、修改同一個 ArrayList 實例時,會導(dǎo)致:

  • 數(shù)據(jù)不一致:例如兩個線程同時添加元素,可能導(dǎo)致 size 值錯誤,甚至覆蓋彼此的數(shù)據(jù)。
  • 數(shù)組越界異常:內(nèi)部數(shù)組擴容時,多個線程同時操作可能導(dǎo)致 ArrayIndexOutOfBoundsException。
  • ConcurrentModificationException:當一個線程正在迭代,另一個線程修改了列表結(jié)構(gòu)(如添加或刪除元素)時,會快速失敗拋出該異常。
List<Integer> list = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> list.add(1));
}
executor.shutdown();
// 結(jié)果:可能拋出異常,或最終 size 不等于 1000

二、為什么它是線程不安全的

ArrayList 是線程不安全的,根本原因在于其內(nèi)部實現(xiàn)沒有對共享數(shù)據(jù)的并發(fā)訪問進行任何同步控制,導(dǎo)致多線程同時修改時會出現(xiàn)數(shù)據(jù)競爭。

2.1 內(nèi)部數(shù)據(jù)結(jié)構(gòu)

ArrayList 底層是一個 Object 數(shù)組elementData)和一個 int 類型的 size 字段,用于記錄實際元素個數(shù):

所有修改操作如 add、remove都會直接操作這個數(shù)組和 size。

2.2 并發(fā)添加時的競態(tài)條件

假設(shè)兩個線程同時執(zhí)行 list.add(e),該方法大致步驟如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 檢查是否需要擴容
    elementData[size++] = e;            // 插入元素并 size 自增
    return true;
}

1. 擴容檢查

  • ensureCapacityInternal(size + 1) 會讀取當前 size,判斷數(shù)組是否已滿。
  • 如果兩個線程同時發(fā)現(xiàn)數(shù)組還有空間(比如 size=5,容量=10),它們都會認為無需擴容,然后繼續(xù)向下執(zhí)行。
  • 但若兩個線程同時發(fā)現(xiàn)需要擴容,它們可能各自進行擴容操作,最終只有一個數(shù)組被保留,另一個線程使用的數(shù)組可能被覆蓋,導(dǎo)致數(shù)據(jù)丟失。

2. 數(shù)組越界異常

更常見的情況是:兩個線程在擴容后都準備插入元素:

  • 線程 A 執(zhí)行 elementData[size] = e 時,size 還是舊值(比如 5);
  • 線程 B 也執(zhí)行同樣的操作,可能線程 A 還沒更新 size,線程 B 仍然使用舊 size(5)寫入同一位置,覆蓋了線程 A 的數(shù)據(jù);
  • 或者線程 B 在寫入前,線程 A 已經(jīng)更新 size 為 6,線程 B 再寫入時使用的索引 5 已經(jīng)合法,但最終 size 可能只增加了一次,導(dǎo)致少計一個元素。
  • 極端情況:如果兩個線程同時寫入同一個數(shù)組下標,并且數(shù)組已滿且都觸發(fā)了擴容,可能最終寫入時使用的數(shù)組引用不一致,導(dǎo)致 ArrayIndexOutOfBoundsException。

3. size 的非原子操作

size++ 實際上分為三步:讀取 size → 加 1 → 寫回 size。多線程環(huán)境下,這些步驟可能交錯執(zhí)行,導(dǎo)致:

  • 兩個線程都讀取到 size=5,各自加 1 后寫回 6,最終 size 為 6,但實際插入了兩個元素,丟失一次更新
  • 最終 size 值小于實際元素個數(shù),或數(shù)組中有空位(null),后續(xù)操作可能出現(xiàn)問題。

三、實際測試

我們看一段如下的java代碼

/**
 * 演示 ArrayList 多線程并發(fā)問題
 */
@Test
public void testArrayListConcurrencyIssue() throws InterruptedException {
    List<Integer> list = new ArrayList<>();
    int threadCount = 5;
    CountDownLatch latch = new CountDownLatch(threadCount);

    // 創(chuàng)建 5 個線程,每個線程添加 1000 個元素
    for (int i = 0; i < threadCount; i++) {
        final int threadId = i;
        new Thread(() -> {
            try {
                for (int j = 0; j < 1000; j++) {
                    list.add(threadId * 1000 + j);
                }
            } catch (Exception e) {
                System.err.println("線程異常:" + e);
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();

    System.out.println("預(yù)期大小:" + (threadCount * 1000));
    System.out.println("實際大?。? + list.size());
    System.out.println("丟失元素:" + (threadCount * 1000 - list.size()));
    
    if (list.size() < threadCount * 1000) {
        System.out.println("? 檢測到線程安全問題:數(shù)據(jù)丟失!");
    }
}

其運行結(jié)果如下:

四、解決方案

關(guān)于ArrayList多線程并發(fā)解決方案還是很多的,但是各有優(yōu)缺點,我們來一個一個介紹。

4.1 Collections.synchronizedList 同步包裝器

它是將普通 ArrayList 轉(zhuǎn)換為線程安全版本最直接的手段。synchronizedList 的核心設(shè)計思路是 裝飾器模式。它并沒有重新實現(xiàn)一個 List,而是將原有的 ArrayList 包裹起來,然后在每個方法的實現(xiàn)上都加上 synchronized 代碼塊,通過同一把 互斥鎖 來保證線程安全。

// Collections類中的靜態(tài)內(nèi)部類
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    final List<E> list;  // 被包裝的原始ArrayList

    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);  // 將mutex(鎖對象)傳給父類
        this.list = list;
    }

    public E get(int index) {
        synchronized (mutex) {  // 獲取鎖
            return list.get(index); // 調(diào)用原ArrayList的方法
        }  // 釋放鎖
    }

    public void add(int index, E element) {
        synchronized (mutex) {  // 獲取鎖
            list.add(index, element); // 調(diào)用原ArrayList的方法
        }  // 釋放鎖
    }
    
    // ... 其他所有方法都是同樣的模式
}

優(yōu)點

  • 使用簡單:一行代碼就能把非線程安全的 ArrayList 包裝成線程安全的。
  • 強一致性:由于每次操作都加鎖,你能獲得 強一致性 的數(shù)據(jù)視圖(只要操作完成,其他線程立即可見)。
  • 兼容性好:返回的 List 實現(xiàn)了 RandomAccess 接口(如果原 List 實現(xiàn)了),所以隨機訪問的性能和 ArrayList 一樣好。

缺點

  • 性能瓶頸:相當于將并發(fā)操作強制變成了串行操作。在并發(fā)量高的時候,這就是一個巨大的性能瓶頸。
  • 粗粒度鎖:鎖的范圍是整個 List 對象,無法進行并發(fā)讀?。ㄗx寫互斥,讀讀也互斥)。

同時它也有著大量的其他問題,例如復(fù)合操作不具備原子性。即使使用了 synchronizedList,下面這段代碼在多線程環(huán)境下仍然是錯誤的:

List<String> list = Collections.synchronizedList(new ArrayList<>());
// ... 假設(shè)list中已經(jīng)有了一些元素

// 在多線程環(huán)境下執(zhí)行這段代碼
if (!list.contains("a")) {  // 檢查操作(已加鎖)
    list.add("b");          // 添加操作(已加鎖)
}

contains 和 add 雖然是兩個原子操作,但組合在一起就不是原子操作了。在 contains 檢查通過后、add 執(zhí)行之前,可能有另一個線程插進來添加了該元素,導(dǎo)致最終重復(fù)添加。

此時你需要手動使用同一個鎖對象來保證復(fù)合操作的原子性:

// 正確做法:使用list對象本身作為鎖,鎖住整個操作塊
synchronized (list) {
    if (!list.contains("特定元素")) {
        list.add("特定元素");
    }
}

因為 synchronizedList 內(nèi)部使用的是 this 作為鎖,所以外部用 synchronized (list) 可以保證與內(nèi)部方法使用的是同一把鎖。這個是多線程的知識點,如果有不會的可以翻翻我寫的關(guān)于多線程的文章。

其次的問題就是遍歷時需要手動加鎖。當使用迭代器遍歷 synchronizedList 時,必須在外層加鎖。

List<String> list = Collections.synchronizedList(new ArrayList<>());

// 錯誤示例:會拋出 ConcurrentModificationException
// 因為迭代器遍歷期間,另一個線程可能修改了list
for (String item : list) { 
    // 處理item
}

// 正確示例
synchronized (list) {
    for (String item : list) { // 在鎖的保護下遍歷
        // 處理item
    }
}

這是因為 SynchronizedList 的 iterator() 方法本身并沒有加鎖,它返回的迭代器在遍歷過程中,如果有其他線程修改了 List,依然會觸發(fā)快速失敗機制。

綜上,如果在使用場景是讀寫比例均衡,或需要強一致性的場景,可以考慮使用Collections.synchronizedList,但是需要記住在遍歷或者任何復(fù)合操作的情況下,都需要手動加鎖來保證原子性。

/**
 * 使用 Collections.synchronizedList 解決并發(fā)問題
 */
@Test
public void testSynchronizedList() throws InterruptedException {
    List<Integer> list = Collections.synchronizedList(new ArrayList<>());
    int threadCount = 10;
    int opsPerThread = 50000;
    CountDownLatch latch = new CountDownLatch(threadCount);

    long startTime = System.currentTimeMillis();

    for (int i = 0; i < threadCount; i++) {
        final int threadId = i;
        new Thread(() -> {
            try {
                for (int j = 0; j < opsPerThread; j++) {
                    synchronized (list) {
                        list.add(threadId * opsPerThread + j);
                    }
                }
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    long endTime = System.currentTimeMillis();

    System.out.println("預(yù)期大?。? + (threadCount * opsPerThread));
    System.out.println("實際大小:" + list.size());
    System.out.println("執(zhí)行時間:" + (endTime - startTime) + "ms");
    
    if (list.size() == threadCount * opsPerThread) {
        System.out.println("? synchronizedList 保證線程安全!");
    }
}

4.2 CopyOnWriteArrayList 并發(fā)集合

這是Java并發(fā)包(JUC)中專門為讀多寫極少場景量身定做的一個方法。它的設(shè)計思想非常巧妙,采用了不變性寫時復(fù)制策略,徹底解決了并發(fā)沖突的問題。

CopyOnWriteArrayList 的核心思想非常簡單直觀:每當需要對列表進行修改(增、刪、改)時,不直接修改原始數(shù)組,而是先復(fù)制一份快照,在快照上修改,修改完成后再將原數(shù)組的引用指向這個新的數(shù)組。

public class CopyOnWriteArrayList<E> {
    // 關(guān)鍵:使用 volatile 修飾的數(shù)組,保證修改后對其他線程的可見性
    private transient volatile Object[] array;

    // 添加元素的方法
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); // 寫操作必須加鎖,防止并發(fā)修改時復(fù)制出多個副本
        try {
            Object[] elements = getArray(); // 獲取當前數(shù)組
            int len = elements.length;
            // 核心:復(fù)制一個新數(shù)組(長度+1)
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e; // 在新數(shù)組上執(zhí)行添加操作
            setArray(newElements); // 將新數(shù)組設(shè)為當前數(shù)組
            return true;
        } finally {
            lock.unlock();
        }
    }

    // 讀取元素的方法(沒有加鎖)
    public E get(int index) {
        return get(getArray(), index); // 直接從當前數(shù)組中獲取
    }

    // 返回當前數(shù)組的快照
    final Object[] getArray() {
        return array;
    }
}

這種設(shè)計帶來的兩個核心特性:

  • 讀操作無鎖:讀線程永遠不需要加鎖,因為它們訪問的是當前時刻的數(shù)組快照。即使此時有寫線程正在復(fù)制新數(shù)組,也完全不影響讀線程訪問舊數(shù)組。
  • 數(shù)據(jù)弱一致性:迭代器一旦被創(chuàng)建,它遍歷的就是創(chuàng)建時刻的那個數(shù)組快照。遍歷過程中,其他線程對列表的修改(即使已經(jīng)提交)對當前迭代器是不可見的。這被稱為  "弱一致性" 。

優(yōu)點

  • 極高的讀并發(fā):讀操作完全不阻塞,也不互斥。這在讀多寫少的場景下,性能遠超 Collections.synchronizedList。
  • 迭代安全:永遠不會拋出 ConcurrentModificationException。因為迭代器操作的是獨立的數(shù)組快照。

缺點

  • 內(nèi)存開銷:每次修改都要復(fù)制整個數(shù)組。如果列表很大(比如上萬元素),頻繁復(fù)制會造成巨大的內(nèi)存壓力(老數(shù)組和正在構(gòu)建的新數(shù)組同時存在于內(nèi)存中),甚至引發(fā)頻繁的GC。
  • 數(shù)據(jù)延遲:寫線程修改數(shù)據(jù)后,并不能保證讀線程立即看到最新數(shù)據(jù)。因為讀線程訪問的可能還是舊的數(shù)組快照。不過由于volatile變量的語義,這個"不可見"的時間窗口非常短(寫完成后,后續(xù)的讀操作一定能看到)。
  • 不適合寫頻繁場景:如果寫操作較多,復(fù)制數(shù)組的開銷會急劇上升,性能可能反而不如 synchronizedList

綜上,如果你的業(yè)務(wù)要求嚴格的實時一致性(比如支付扣款后的余額查詢),CopyOnWriteArrayList 就不適合了。

特性CopyOnWriteArrayListCollections.synchronizedList
實現(xiàn)原理空間換時間:寫時復(fù)制,讀寫分離時間換安全:所有操作串行化
讀鎖無鎖有鎖(讀讀互斥)
寫鎖有鎖(用ReentrantLock控制)有鎖
內(nèi)存占用高(每次寫創(chuàng)建新數(shù)組)
數(shù)據(jù)一致性弱一致性(迭代器快照)強一致性(鎖保護)
迭代異常永不拋出 ConcurrentModificationException遍歷期間如有修改會拋出異常
最佳場景讀多寫極少(配置、白名單、監(jiān)聽器列表)讀寫均衡,或需要強一致性的場景
/**
 * 演示使用 CopyOnWriteArrayList 解決并發(fā)問題
 */
@Test
public void testThreadSafeList() throws InterruptedException {
    List<Integer> list = new CopyOnWriteArrayList<>();
    int threadCount = 10;
    int opsPerThread = 50000;
    CountDownLatch latch = new CountDownLatch(threadCount);

    long startTime = System.currentTimeMillis();

    for (int i = 0; i < threadCount; i++) {
        final int threadId = i;
        new Thread(() -> {
            try {
                for (int j = 0; j < opsPerThread; j++) {
                    list.add(threadId * opsPerThread + j);
                }
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    long endTime = System.currentTimeMillis();

    System.out.println("預(yù)期大?。? + (threadCount * opsPerThread));
    System.out.println("實際大?。? + list.size());
    System.out.println("執(zhí)行時間:" + (endTime - startTime) + "ms");
    
    if (list.size() == threadCount * opsPerThread) {
        System.out.println("? 線程安全,數(shù)據(jù)完整!");
    }else {
        System.out.println("? 檢測到線程安全問題:數(shù)據(jù)不完整!");
    }
}

通過上述代碼可以看到,同樣是50萬量級的數(shù)據(jù),使用CopyOnWriteArrayList的運行插入的時間是Collections.synchronizedList好幾百倍,所以我們一定要注意使用場景的問題。

我們再來用一個讀多寫少的場景對比一下

/**
 * 讀多寫少場景:synchronizedList vs CopyOnWriteArrayList
 */
@Test
public void testReadHeavyScenario() throws InterruptedException {
    int threadCount = 10;
    int writeCount = 1000;
    int readCount = 1000000;

    System.out.println("=== synchronizedList 讀多寫少 ===");
    long syncTime = testSynchronizedList(threadCount, writeCount, readCount);

    System.out.println("\n=== CopyOnWriteArrayList 讀多寫少 ===");
    long cowTime = testCopyOnWriteArrayList(threadCount, writeCount, readCount);

    System.out.println("\n=== 性能對比 ===");
    System.out.println("synchronizedList: " + syncTime + "ms");
    System.out.println("CopyOnWriteArrayList: " + cowTime + "ms");
    System.out.println("CopyOnWriteArrayList " + (syncTime > cowTime ? "更快" : "更慢") + 
                      ",提升了 " + String.format("%.2f", (double)(syncTime - cowTime) / syncTime * 100) + "%");
}

private long testSynchronizedList(int threadCount, int writeCount, int readCount) throws InterruptedException {
    List<Integer> list = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch latch = new CountDownLatch(threadCount);
    long startTime = System.currentTimeMillis();

    for (int i = 0; i < threadCount; i++) {
        final int threadId = i;
        new Thread(() -> {
            try {
                for (int j = 0; j < writeCount; j++) {
                    synchronized (list) {
                        list.add(threadId * writeCount + j);
                    }
                }
                for (int j = 0; j < readCount; j++) {
                    synchronized (list) {
                        if (!list.isEmpty()) {
                            list.get(list.size() - 1);
                        }
                    }
                }
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    long endTime = System.currentTimeMillis();
    System.out.println("寫+讀執(zhí)行時間:" + (endTime - startTime) + "ms,寫大?。? + list.size());
    return endTime - startTime;
}

private long testCopyOnWriteArrayList(int threadCount, int writeCount, int readCount) throws InterruptedException {
    List<Integer> list = new CopyOnWriteArrayList<>();
    CountDownLatch latch = new CountDownLatch(threadCount);
    long startTime = System.currentTimeMillis();

    for (int i = 0; i < threadCount; i++) {
        final int threadId = i;
        new Thread(() -> {
            try {
                for (int j = 0; j < writeCount; j++) {
                    list.add(threadId * writeCount + j);
                }
                for (int j = 0; j < readCount; j++) {
                    if (!list.isEmpty()) {
                        list.get(list.size() - 1);
                    }
                }
            } finally {
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    long endTime = System.currentTimeMillis();
    System.out.println("寫+讀執(zhí)行時間:" + (endTime - startTime) + "ms,寫大?。? + list.size());
    return endTime - startTime;
}

上面的程序是十個線程,寫入1萬,讀100萬,可以看到在讀百萬量級數(shù)據(jù)的時候,用CopyOnWriteArrayList的時間幾乎是提升了40倍。

以上就是Java Arraylist在多線程環(huán)境下的問題與解決方案的詳細內(nèi)容,更多關(guān)于Java Arraylist線程不安全性的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論

福建省| 桃江县| 丹棱县| 绥化市| 达尔| 洛南县| 商丘市| 大冶市| 黄平县| 五大连池市| 竹山县| 宁安市| 安龙县| 上林县| 麦盖提县| 惠安县| 南郑县| 克山县| 晴隆县| 延川县| 友谊县| 乌拉特中旗| 新巴尔虎左旗| 阿坝县| 定西市| 兴业县| 德令哈市| 和静县| 中宁县| 平南县| 岑溪市| 饶平县| 龙口市| 临泉县| 延庆县| 兴山县| 册亨县| 菏泽市| 吐鲁番市| 湘潭市| 凤翔县|