15-同步容器

同步容器

爲什麼會出現同步容器

在Java的集合框架中,主要有四大類別:List、Set、Queue、Map。

List、Set、Queue接口分別繼承了Collection接口,Map本身是一個接口。注意Collection和Map是一個頂層接口,而List、Set、Queue則繼承了Collection接口,分別代表數組、集合和隊列這三大類容器。像ArrayList、LinkedList都是實現了List接口,HashSet實現了Set接口,而Deque(雙向隊列:允許在隊首、隊尾進行入隊和出隊操作)繼承了Queue接口,PriorityQueue實現了Queue接口。另外LinkedList(實際上是雙向鏈表)實現了了Deque接口。

像ArrayList、LinkedList、HashMap這些容器都是非線程安全的。如果有多個線程併發地訪問這些容器時,就會出現問題。因此,在編寫程序時,必須要求程序員手動地在任何訪問到這些容器的地方進行同步處理,這樣導致在使用這些容器的時候非常地不方便。所以,Java提供了同步容器供用戶使用。

java中的同步容器類

在Java中,同步容器主要包括2類:

1)Vector、Stack、HashTable

2)Collections工具類中提供的靜態工廠方法創建的類

Vector實現了List接口,Vector實際上就是一個數組,和ArrayList類似,但是Vector中的方法都是synchronized方法,即進行了同步措施。

Stack也是一個同步容器,它的方法也用synchronized進行了同步,它實際上是繼承於Vector類。

HashTable實現了Map接口,它和HashMap很相似,但是HashTable進行了同步處理,而HashMap沒有。

Collections是一個工具類,注意,它和Collection不同,Collection是一個頂層的接口。在Collections類中提供了大量的方法,比如對集合或者容器進行排序、查找等操作。最重要的是,在它裏面提供了幾個靜態工廠方法來創建同步容器類:

 public static Collection synchronizedCollention(Collection c)
 public static List synchronizedList(list l)
 public static Map synchronizedMap(Map m)
 public static Set synchronizedSet(Set s)
 public static SortedMap synchronizedSortedMap(SortedMap sm)
 public static SortedSet synchronizedSortedSet(SortedSet ss)

Collections同步工具類

爲了創建線程安全且由ArrayList支持的List,可以使用如下代碼:

List list = Collection.synchronizedList(new ArrayList());

注意,ArrayList實例馬上封裝起來,不存在對未同步的ArrayList的直接引用(即直接封裝匿名實例)。這是一種最安全的途徑。如果另一個線程可以直接引用ArrayList實例,它可以執行非同步修改。

下面給出一段多線程中安全遍歷集合元素的示例。我們使用Iterator逐個掃描List中的元素,在多線程環境中,當遍歷當前集合中的元素時,一般希望阻止其他線程添加或刪除元素。安全遍歷的實現方法如下:

public class Test {

    public static void main(String[] args) {
        // 爲了安全起見,僅使用同步列表的一個引用,這樣可以確保控制了所有訪問  
        // 集合必須同步化,這裏是一個List  
        List<String> wordList = Collections.synchronizedList(new ArrayList<String>());  

        //wordList中的add方法是同步方法,會獲取wordList實例的對象鎖  
        wordList.add("Iterators");  
        wordList.add("require");  
        wordList.add("special");  
        wordList.add("handling");  

        // 獲取wordList實例的對象鎖,  
        // 迭代時,阻塞其他線程調用add或remove等方法修改元素  
        synchronized (wordList) {  
            Iterator<String> iter = wordList.iterator();  
            while (iter.hasNext()) {
                String s = iter.next();  
                System.out.println("found string: " + s + ", length=" + s.length());  
            }
        }
    }
}

同步容器的缺陷

從同步容器的具體實現源碼可知,同步容器中的方法採用了synchronized進行同步,那麼很顯然,這必然會影響到執行性能,另外,同步容器就一定是真正地完全線程安全嗎?不一定,這個在下面會講到。

我們首先來看一下傳統的非同步容器和同步容器的性能差異,我們以ArrayList和Vector爲例:

性能問題

我們先通過一個例子看一下Vector和ArrayList在插入數據時性能上的差異:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        Vector<Integer> vector = new Vector<Integer>();

        long start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            list.add(i);
        long end = System.currentTimeMillis();
        System.out.println("ArrayList進行100000次插入操作耗時:" + (end - start) + "ms");

        start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            vector.add(i);
        end = System.currentTimeMillis();
        System.out.println("Vector進行100000次插入操作耗時:" + (end - start) + "ms");
    }
}

執行結果:

進行同樣多的插入操作,Vector的耗時是ArrayList的兩倍。這只是其中的一方面性能問題上的反映。

另外,由於Vector中的add方法和get方法都進行了同步,因此,在有多個線程進行訪問時,如果多個線程都只是進行讀取操作,那麼每個時刻就只能有一個線程進行讀取,其他線程便只能等待,這些線程必須競爭同一把鎖。

同步容器真的是安全的嗎

也有人認爲Vector中的方法都進行了同步處理,那麼一定就是線程安全的,事實上這可不一定。看下面這段代碼:

public class Test {

    public static void main(String[] args) {

        final Vector<Integer> vector = new Vector<Integer>();

        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);

            Thread thread1 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.remove(i);
                };
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.get(i);
                };
            };

            thread1.start();
            thread2.start();

            while(Thread.activeCount() > 10)   {

            }
        }
    }
}

執行結果:

正如大家所看到的,這段代碼報錯了:數組下標越界。

也許有朋友會問:Vector是線程安全的,爲什麼還會報這個錯?很簡單,對於Vector,雖然能保證每一個時刻只能有一個線程訪問它,但是不排除這種可能:

當某個線程在某個時刻執行這句時:

for(int i=0; i<vector.size(); i++)
    vector.get(i);

假若此時vector的size方法返回的是10,i的值爲9。然後另外一個線程執行了這句:

for(int i=0; i<vector.size(); i++)
    vector.remove(i);

將下標爲9的元素刪除了。那麼通過get方法訪問下標爲9的元素肯定就會出問題了。

因此爲了保證線程安全,必須在方法調用端做額外的同步措施,如下面所示:

public class Test {

    public static void main(String[] args) {

        final Vector<Integer> vector = new Vector<Integer>();

        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);

            Thread thread1 = new Thread() {
                public void run() {
                    synchronized (Test.class) { // 額外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.remove(i);
                    }
                };
            };
            Thread thread2 = new Thread() {
                public void run() { 
                    synchronized (Test.class) { // 額外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.get(i);
                    }
                };
            };

            thread1.start();
            thread2.start();

            while(Thread.activeCount() > 10)   {

            }
        }
    }
}

這個例子也說明了:由兩個原子操作組成的複合操作不再是原子操作,如果需要保持原子性,則需要進行額外的同步。

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