如何線程安全地遍歷List(或其他容器),fastfail機制

遍歷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);
});

方式一的遍歷方法對于 RandomAccess 接口的實現(xiàn)類(例如 ArrayList)來說是一種性能很好的遍歷方式。但是對于 LinkedList 這樣的基于鏈表實現(xiàn)的 List,通過 list.get(i) 獲取元素的性能差。

方式二和方式三兩種方式的本質(zhì)是一樣的,都是通過 Iterator 迭代器來實現(xiàn)的遍歷,方式三是增強版的 for 循環(huán),可以看作是方式二的簡化形式。

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

遍歷List的同時操作List會發(fā)生什么?
先用非線程安全的 ArrayList 做個試驗,用一個線程通過增強的 for 循環(huán)遍歷 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秒來調(diào)慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
    
    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由于程序跑的太快,這里sleep了1秒來調(diào)慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結(jié)果:

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

線程一在遍歷到第二個元素時,線程二刪除了一個元素,此時程序出現(xiàn)異常: ConcurrentModificationException 。

當一個 List 正在通過迭代器遍歷時,同時另外一個線程對這個 List 進行修改,就會發(fā)生異常。

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

將程序中的:

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

改成:

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

再運行一次試試,會發(fā)現(xiàn)結(jié)果和 ArrayList 一樣會拋出 ConcurrentModificationException 異常。

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

如果想要線程安全地遍歷 Vector,需要我們?nèi)ナ謩釉诒闅v時給 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秒來調(diào)慢程序的運行速度
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }).start();
    
    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由于程序跑的太快,這里sleep了1秒來調(diào)慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結(jié)果:

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

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

CopyOnWriteArrayList
CopyOnWriteArrayList 是 java.util.concurrent 包中的一個 List 的實現(xiàn)類。CopyOnWrite 的意思是在寫時拷貝,也就是如果需要對CopyOnWriteArrayList 的內(nèi)容進行改變,首先會拷貝一份新的 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秒來調(diào)慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
    
    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由于程序跑的太快,這里sleep了1秒來調(diào)慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結(jié)果:
遍歷元素:0
遍歷元素:1
list.remove(4)
遍歷元素:2
遍歷元素:3
遍歷元素:4

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

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

由于 forEach 方法是 List 內(nèi)部的一個方法,所以不同于在 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秒來調(diào)慢程序的運行速度
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }).start();
    
    // 線程二:remove一個元素
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 由于程序跑的太快,這里sleep了1秒來調(diào)慢程序的運行速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            list.remove(4);
            System.out.println("list.remove(4)");
        }
    }).start();
}

運行結(jié)果:
遍歷元素:0
遍歷元素:1
遍歷元素:2
遍歷元素:3
遍歷元素:4
list.remove(4)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容