Java多線程之併發容器:CopyOnWrite到底幹啥用的

CopyOnWrite從字面上理解就是寫入的時候做複製操作。而CopyOnWrite是一個Java5之後出現的併發容器,目的是爲了提高併發的存取效率。對比CopyOnWrite、ArrayList和Vector源碼,可以發現Vector是get和set方法都使用了synchronized關鍵字做了同步,ArrayList都沒有用該關鍵字,很明顯線程不安全;查看CopyOnWrite源碼,get方法沒有同步,add方法做了同步,也就是說CopyOnWrite的寫入操作完全不影響get操作(一種讀寫分離的思想)。
起初我也很不明白該容器是有這個好處,但在什麼場景下使用呢?
思考一個場景,一個比較穩定的白名單或者黑名單list,每晚或每週會更新一次,其他時間均被大量線程在讀取,這個時候我們用什麼合適?併發場景ArrayList首先排除,Vector性能存在問題,官方也不推薦用了,也排查;Collections.synchronizedList呢,該併發容器做了改進,get和add方法均爲同步方法,如果平時的讀取操作也要走同步那就很不合適了,影響性能,那貌似值剩下CopyOnWrite了。對於這種讀多寫少的情況,用CopyOnWrite再合適不過了。但是,是否有人會問爲什麼CopyOnWrite的add操作是直接複製一份,再做寫操作,爲什麼不直接add呢?這是爲了在寫入時完全不影響讀取操作,在所有的數據都寫入完畢後讀操作才能讀取到最新的數據。
下面是一個CopyOnWrite的使用例子:

package copyonwrite;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by dingxiangyong on 2016/3/26.
 */
public class Test {
    /**
     * 結束標識
     */
    static volatile boolean stopFlag = false;

    public static void main(String[] args) {

        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();
        //初始化集合
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }

        ReadTask readTask1 = new ReadTask(list);
        ReadTask readTask2 = new ReadTask(list);
        ReadTask readTask3 = new ReadTask(list);
        ReadTask readTask4 = new ReadTask(list);

        WriteTask writeTask = new WriteTask(list);

        ExecutorService service = Executors.newFixedThreadPool(5);
        service.execute(readTask1);
        service.execute(readTask2);
        service.execute(readTask3);
        service.execute(readTask4);
        service.execute(writeTask);

        service.shutdown();
    }
}

/**
 * 讀線程
 */
class ReadTask implements Runnable {

    private List<Integer> list;

    public ReadTask(List<Integer> list) {
        this.list = list;
    }


    @Override
    public void run() {
        while (!Test.stopFlag) {
            try {
                int index = (int) (Math.random() * list.size());
                Integer value = list.get(index);
                System.out.println("正在讀取值:" + value);
                Thread.sleep(10); //模擬耗時操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 寫線程
 */
class WriteTask implements Runnable {

    private List<Integer> list;

    public WriteTask(List<Integer> list) {
        this.list = list;
    }


    @Override
    public void run() {
        List<Integer> newList = new ArrayList<Integer>();

        for (int i = 100000; i < 2000000; i++) {
            newList.add(i);
        }

        try {
            Thread.sleep(100); //模擬耗時操作,讓讀線程可以讀到
            System.out.println("準備寫入copyonwritelist");
            list.addAll(newList);
            System.out.println("寫入copyonwritelist完畢");
            Thread.sleep(2000); //模擬耗時操作,讓讀線程可以讀到
            Test.stopFlag = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

....
正在讀取值:79029
正在讀取值:29270
正在讀取值:61694
正在讀取值:3373
正在讀取值:33155
正在讀取值:78532
正在讀取值:12583
正在讀取值:2778
正在讀取值:32738
正在讀取值:82363
準備寫入copyonwritelist
正在讀取值:61661
正在讀取值:5817
正在讀取值:84120
正在讀取值:43250
寫入copyonwritelist完畢
正在讀取值:1744710
正在讀取值:422552
正在讀取值:1038943
正在讀取值:1229489
正在讀取值:200887
正在讀取值:1547701
正在讀取值:1841575
正在讀取值:1785725
...

從如上結果可以看出,在寫入數據的時候,讀取操作並沒有被終止,寫入完畢後讀取操作可以讀取到最新的數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章