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

遍歷List的多種方式

在講如何線程安全地遍歷List以前,先看看一般咱們遍歷一個List會採用哪些方式。java

方式一:

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接口的實現類(例如ArrayList)來講是一種性能很好的遍歷方式。可是對於LinkedList這樣的基於鏈表實現的List,經過list.get(i)獲取元素的性能差。安全

方式二和方式三兩種方式的本質是同樣的,都是經過Iterator迭代器來實現的遍歷,方式三是加強版的for循環,能夠看做是方式二的簡化形式。dom

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

遍歷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.ConcurrentModificationExceptionspa

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

試想若是一個老師正在點整個班級全部學生的人數(線程一遍歷List),而校長(線程二)同時叫走幾個學生,那麼老師也確定點不下去了。code

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

使用線程安全的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/

相關文章
相關標籤/搜索