如何線程安全地遍歷List:Vector、CopyOnWriteArrayList

来源:http://www.cnblogs.com/wucao/archive/2016/04/03/5350461.html
-Advertisement-
Play Games

遍歷List的多種方式 在講如何線程安全地遍歷List之前,先看看通常我們遍歷一個List會採用哪些方式。 方式一: 方式二: 方式三: 方式四(Java 8): 方式五(Java 8 Lambda): 首先,方式一的遍歷方法是一種非常不建議使用的方式,特別是對於LinkedList。LinkedL ...


遍歷List的多種方式

在講如何線程安全地遍歷List之前,先看看通常我們遍歷一個List會採用哪些方式。

方式一:

for(int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

方式二:

Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

方式三:

for(Object item : list) {
    System.out.println(item);
}

方式四(Java 8):

list.forEach(new Consumer<Object>() {
    @Override
    public void accept(Object item) {
        System.out.println(item);
    }
});

方式五(Java 8 Lambda):

list.forEach(item -> {
    System.out.println(item);
});

首先,方式一的遍歷方法是一種非常不建議使用的方式,特別是對於LinkedList。LinkedList是基於鏈表實現的,通過list.get(i)獲取元素的性能差,所以方式一是性能很差的一種遍歷方式。本文接下來的內容不再討論這種方式的遍歷。

方式二和方式三兩種方式的本質是一樣的,都是通過Iterator迭代器來實現的遍歷,方式三可以看作是方式二的簡化形式。

方式四和方式五本質也是一樣的,都是使用Java 8新增的forEach方法來遍歷。方式五是方式四的一種簡化形式,使用了Lambda表達式。

遍歷List的同時操作List會發生什麼?

先用非線程安全的ArrayList做個試驗,用一個線程遍歷List,遍歷的同時另一個線程刪除List中的一個元素,代碼如下:

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new ArrayList<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            for(int item : list) {
                System.out.println("遍歷元素:" + item);
                // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結果: 

遍歷元素:0 
遍歷元素:1 
list.remove(4) 
Exception in thread “Thread-0” java.util.ConcurrentModificationException

線程一在遍歷到第二個元素時,線程二刪除了一個元素,此時程式出現異常:ConcurrentModificationException。

試想如果一個老師正在點整個班級所有學生的人數(線程一遍歷List),而校長(線程二)同時叫走幾個學生,那麼老師也肯定點不下去了。

所以我們會想到一個解決方案,那就是校長等待老師點完學生後,再叫走學生。即讓線程二等待線程一的遍歷完成後再進行remove元素。

使用線程安全的Vector

ArrayList是非線程安全的,Vector是線程安全的,那麼把ArrayList換成Vector是不是就可以線程安全地遍歷了?

將程式中的:

final List<Integer> list = new ArrayList<>();

改成:

final List<Integer> list = new Vector<>();

再運行一次試試,會發現結果和ArrayList一樣會拋出ConcurrentModificationException異常。

為什麼線程安全的Vector也不能線程安全地遍歷呢?其實道理也很簡單,看Vector源碼可以發現它的很多方法都加上了synchronized來進行線程同步,例如add()、remove()、set()、get(),但是Vector內部的synchronized方法無法控制到遍歷操作,所以即使是線程安全的Vector也無法做到線程安全地遍歷。

如果想要線程安全地遍歷Vector,需要我們去手動在遍歷時給Vector加上synchronized鎖,防止遍歷的同時進行remove操作。相當於校長等待老師點完學生後,再叫走學生。代碼如下:

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new Vector<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            // synchronized來鎖住list,remove操作會在遍歷完成釋放鎖後進行
            synchronized (list) {
                for(int item : list) {
                    System.out.println("遍歷元素:" + item);
                    // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結果:

遍歷元素:0 
遍歷元素:1 
遍歷元素:2 
遍歷元素:3 
遍歷元素:4 
list.remove(4)

運行結果顯示list.remove(4)的操作是等待遍歷完成後再進行的。

CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一個List的實現類。CopyOnWrite的意思是在寫時拷貝,也就是如果需要對CopyOnWriteArrayList的內容進行改變,首先會拷貝一份新的List並且在新的List上進行修改,最後將原List的引用指向新的List。

使用CopyOnWriteArrayList可以線程安全地遍歷,因為如果另外一個線程在遍歷的時候修改List的話,實際上會拷貝出一個新的List上修改,而不影響當前正在被遍歷的List。

相當於校長要想從班級喊走或者添加學生,需要把學生全部帶到一個新的教室再進行操作,而老師則通過之前班級的快照在照片上清點學生。

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new CopyOnWriteArrayList<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            for(int item : list) {
                System.out.println("遍歷元素:" + item);
                // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結果:

遍歷元素:0 
遍歷元素:1 
list.remove(4) 
遍歷元素:2 
遍歷元素:3 
遍歷元素:4

從上面的運行結果可以看出,雖然list.remove(4)已經移除了一個元素,但是遍歷的結果還是存在這個元素。由此可以看出被遍歷的和remove的是兩個不同的List。

線程安全的List.forEach

List.forEach方法是Java 8新增的一個方法,主要目的還是用於讓List來支持Java 8的新特性:Lambda表達式。

由於forEach方法是List的一個方法,所以不同於在List外遍歷List,forEach方法相當於List自身遍歷的方法,所以它可以自由控制是否線程安全。

我們看線程安全的Vector的forEach方法源碼:

public synchronized void forEach(Consumer<? super E> action) {
    ...
}

可以看到Vector的forEach方法上加了synchronized來控制線程安全的遍歷,也就是Vector的forEach方法可以線程安全地遍歷

下麵可以測試一下:

public static void main(String[] args) {

    // 初始化一個list,放入5個元素
    final List<Integer> list = new Vector<>();
    for(int i = 0; i < 5; i++) {
        list.add(i);
    }

    // 線程一:通過Iterator遍歷List
    new Thread(new Runnable() {
        @Override
        public void run() {
            list.forEach(item -> {
                System.out.println("遍歷元素:" + item);
                // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }).start();

    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由於程式跑的太快,這裡sleep了1秒來調慢程式的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結果: 

遍歷元素:0 
遍歷元素:1 
遍歷元素:2 
遍歷元素:3 
遍歷元素:4 
list.remove(4)

轉載請註明原文地址:http://xxgblog.com/2016/04/02/traverse-list-thread-safe/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原文鏈接:http://www.orlion.ga/189/ 一、scope bean的scope屬性中常用的有兩種:singleton(單例,預設)和prototype(原型,每次創建新對象) 例:beans.xml 在java文件中: 二、集合註入 UserDAOImpl.java: beans ...
  • 概述 在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。然而在這種簡單 ...
  • 原文鏈接:http://www.orlion.ga/689/ 好久之前就知道有這麼個東西,但是一直沒用,一直用exit()、var_dump() debug,效率很低。 首先下載xdebug的dll文件(Window環境下)網址是:https://xdebug.org/download.php,此次 ...
  • 原文鏈接:http://www.orlion.ga/776/ 用C寫的程式效率可能不如彙編,而且有些平臺相關的指令必須手寫,例如x86是埠I/O,而c語言就沒有這個概念,所以in/out指令必須用彙編來寫。 gcc提供了一種擴展寫法可以在C代碼中試用內聯彙編,最簡單的格式是__asm__("ass ...
  • 1,在python中#以井號鍵開頭的是註釋的內容,解釋器不會管他; 2,在python中:以冒號結尾時,後面的縮進為其代碼塊,這是約定熟成的習慣,並且堅持一個縮進頂4個空格。(sublime Text設置一個tab頂4個空格:在preference——>seting-user中,在花括弧中添加如下一 ...
  • 基礎知識(包括但不限於:二叉查找樹是啥,SBT又是啥反正又不能吃,平衡樹怎麼旋轉,等等)在這裡就不(lan)予(de)贅(duo)述(xie)了。 由於學生黨比較忙,所以博文寫的比較簡略,有時間會慢慢補完 先貼代碼: 1 int seed; 2 int _rand() 3 { 4 return se ...
  • 2nd
    02.01_Java語言基礎(常量的概述和使用)(掌握) A:什麼是常量 在程式執行的過程中其值不可以發生改變 B:Java中常量的分類 字面值常量 自定義常量(面向對象部分講) C:字面值常量的分類 字元串常量 用雙引號括起來的內容 整數常量 所有整數 小數常量 所有小數 字元常量 用單引號括起來 ...
  • ggg fffffffffffffffffffffffffffffffffffffffffffffff ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...