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

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

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

一、ArrayList 的線程不安全性

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

  • 數(shù)據(jù)不一致:例如兩個(gè)線程同時(shí)添加元素,可能導(dǎo)致 size 值錯(cuò)誤,甚至覆蓋彼此的數(shù)據(jù)。
  • 數(shù)組越界異常:內(nèi)部數(shù)組擴(kuò)容時(shí),多個(gè)線程同時(shí)操作可能導(dǎo)致 ArrayIndexOutOfBoundsException
  • ConcurrentModificationException:當(dāng)一個(gè)線程正在迭代,另一個(gè)線程修改了列表結(jié)構(gòu)(如添加或刪除元素)時(shí),會(huì)快速失敗拋出該異常。
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)部實(shí)現(xiàn)沒有對(duì)共享數(shù)據(jù)的并發(fā)訪問進(jìn)行任何同步控制,導(dǎo)致多線程同時(shí)修改時(shí)會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)。

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

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

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

2.2 并發(fā)添加時(shí)的競(jìng)態(tài)條件

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

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

1. 擴(kuò)容檢查

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

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

更常見的情況是:兩個(gè)線程在擴(kuò)容后都準(zhǔn)備插入元素:

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

3. size 的非原子操作

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

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

三、實(shí)際測(cè)試

我們看一段如下的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 個(gè)線程,每個(gè)線程添加 1000 個(gè)元素
    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("實(shí)際大?。? + list.size());
    System.out.println("丟失元素:" + (threadCount * 1000 - list.size()));
    
    if (list.size() < threadCount * 1000) {
        System.out.println("? 檢測(cè)到線程安全問題:數(shù)據(jù)丟失!");
    }
}

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

四、解決方案

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

4.1 Collections.synchronizedList 同步包裝器

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

// 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(鎖對(duì)象)傳給父類
        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)點(diǎn)

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

缺點(diǎn)

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

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

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

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

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

此時(shí)你需要手動(dòng)使用同一個(gè)鎖對(duì)象來(lái)保證復(fù)合操作的原子性:

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

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

其次的問題就是遍歷時(shí)需要手動(dòng)加鎖。當(dāng)使用迭代器遍歷 synchronizedList 時(shí),必須在外層加鎖。

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

// 錯(cuò)誤示例:會(huì)拋出 ConcurrentModificationException
// 因?yàn)榈鞅闅v期間,另一個(gè)線程可能修改了list
for (String item : list) { 
    // 處理item
}

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

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

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

/**
 * 使用 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("實(shí)際大?。? + list.size());
    System.out.println("執(zhí)行時(shí)間:" + (endTime - startTime) + "ms");
    
    if (list.size() == threadCount * opsPerThread) {
        System.out.println("? synchronizedList 保證線程安全!");
    }
}

4.2 CopyOnWriteArrayList 并發(fā)集合

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

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

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

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

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

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

這種設(shè)計(jì)帶來(lái)的兩個(gè)核心特性:

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

優(yōu)點(diǎn)

  • 極高的讀并發(fā):讀操作完全不阻塞,也不互斥。這在讀多寫少的場(chǎng)景下,性能遠(yuǎn)超 Collections.synchronizedList。
  • 迭代安全:永遠(yuǎn)不會(huì)拋出 ConcurrentModificationException。因?yàn)榈鞑僮鞯氖仟?dú)立的數(shù)組快照。

缺點(diǎn)

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

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

特性CopyOnWriteArrayListCollections.synchronizedList
實(shí)現(xiàn)原理空間換時(shí)間:寫時(shí)復(fù)制,讀寫分離時(shí)間換安全:所有操作串行化
讀鎖無(wú)鎖有鎖(讀讀互斥)
寫鎖有鎖(用ReentrantLock控制)有鎖
內(nèi)存占用高(每次寫創(chuàng)建新數(shù)組)
數(shù)據(jù)一致性弱一致性(迭代器快照)強(qiáng)一致性(鎖保護(hù))
迭代異常永不拋出 ConcurrentModificationException遍歷期間如有修改會(huì)拋出異常
最佳場(chǎng)景讀多寫極少(配置、白名單、監(jiān)聽器列表)讀寫均衡,或需要強(qiáng)一致性的場(chǎng)景
/**
 * 演示使用 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("實(shí)際大?。? + list.size());
    System.out.println("執(zhí)行時(shí)間:" + (endTime - startTime) + "ms");
    
    if (list.size() == threadCount * opsPerThread) {
        System.out.println("? 線程安全,數(shù)據(jù)完整!");
    }else {
        System.out.println("? 檢測(cè)到線程安全問題:數(shù)據(jù)不完整!");
    }
}

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

我們?cè)賮?lái)用一個(gè)讀多寫少的場(chǎng)景對(duì)比一下

/**
 * 讀多寫少場(chǎng)景: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=== 性能對(duì)比 ===");
    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í)行時(shí)間:" + (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í)行時(shí)間:" + (endTime - startTime) + "ms,寫大小:" + list.size());
    return endTime - startTime;
}

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

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

相關(guān)文章

最新評(píng)論

冕宁县| 揭阳市| 格尔木市| 宁阳县| 如东县| 耒阳市| 巴青县| 军事| 武宣县| 团风县| 沂水县| 崇仁县| 普陀区| 丘北县| 汉中市| 凤凰县| 游戏| 沅陵县| 凌海市| 临武县| 饶河县| 永昌县| 南和县| 团风县| 会昌县| 云安县| 新泰市| 鹤山市| 遂平县| 汝阳县| 洪雅县| 巴中市| 上蔡县| 曲阳县| 柞水县| 嵊州市| 平和县| 太保市| 周至县| 大荔县| 湖口县|