1. 程式人生 > >同步類容器 & 併發類容器

同步類容器 & 併發類容器

目錄

一.同步類容器

二.併發類容器

2.1 ConcurrentMap

2.2 Copy-On-Write容器


一.同步類容器

同步類容器是執行緒安全的,但在某些場景下可能需要加鎖來保護複合操作,例如:迭代(反覆訪問元素,元素的遍歷)、跳轉(根據指定的順序找到當前元素的下一個元素)以及條件運算。這些複合操作在多執行緒併發的修改容器時,可能會表現出意外的行為,最經典的是ConcurrentModificationException,原因是當容器迭代的過程中,被併發的修改了內容,這是由於早期迭代器設計的時候並沒有考慮到併發修改的問題。看如下的程式碼,Vector雖然是執行緒安全的,但是在複合操作(迭代加修改元素)的時候,也是會丟擲ConcurrentModificationException異常:

public class Tickets {

    public static void main(String[] args) {
        //初始化火車票池並新增火車票:避免執行緒同步可採用Vector替代ArrayList  Hashtable替代HashMap

        final Vector<String> tickets = new Vector<String>();

        for (int i = 1; i <= 1000; i++) {
            tickets.add("火車票" + i);
        }

        for (Iterator iterator = tickets.iterator(); iterator.hasNext(); ) {
            String string = (String) iterator.next();
            System.out.println("當前元素:" + string);
            tickets.remove(20);
        }

    }
}

多執行緒非複合操作下,vector是執行緒安全的,如下例項程式碼:

public class Tickets {

    public static void main(String[] args) {
        //初始化火車票池並新增火車票:避免執行緒同步可採用Vector替代ArrayList  HashTable替代HashMap

        final Vector<String> tickets = new Vector<String>();


        for (int i = 1; i <= 100000; i++) {
            tickets.add("火車票" + i);
        }


		for(int i = 1; i <=10; i ++){
			new Thread("執行緒"+i){
				public void run(){
					while(true){
						if(tickets.isEmpty()) break;
						System.out.println(Thread.currentThread().getName() + "---" + tickets.remove(0));
					}
				}
			}.start();
		}
    }
}

各個執行緒不斷的去移除元素,並未出現異常。

同步類容器:如古老的Vector、Hashtable。這些容器的同步功能其實都是有JDK的Collection.synchronized等工廠方法去建立實現的。其底層的機制無非就是用傳統的synchronized關鍵字對每個公共的方法都進行同步,使得每次操作只能有一個執行緒去訪問容器的狀態。這很明顯不滿足我們今天網際網路時代高併發的需求,無法保證足夠的效能。

假如我們直接使用ArrayList,做併發的刪除操作,會出現併發問題,程式碼如下:

public class Tickets {

    public static void main(String[] args) {
        //初始化火車票池並新增火車票:避免執行緒同步可採用Vector替代ArrayList  HashTable替代HashMap

        List<String> tickets = new ArrayList<String>();

        for (int i = 1; i <= 100000; i++) {
            tickets.add("火車票" + i);
        }


        for (int i = 1; i <= 10; i++) {
            new Thread("執行緒" + i) {
                public void run() {
                    while (true) {
                        if (tickets.isEmpty()) break;
                        System.out.println(Thread.currentThread().getName() + "---" + tickets.remove(0));
                    }
                }
            }.start();
        }
    }
}

輸出的結果中,會出現列印為空的元素,從而證明ArrayList不是執行緒安全的。此時我們可以使用Collections.synchronizedList方法,將tickets設定為執行緒安全的。

public class Tickets {

    public static void main(String[] args) {
        //初始化火車票池並新增火車票:避免執行緒同步可採用Vector替代ArrayList  HashTable替代HashMap

        List<String> tickets = Collections.synchronizedList(new ArrayList<>());

        for (int i = 1; i <= 100000; i++) {
            tickets.add("火車票" + i);
        }


        for (int i = 1; i <= 10; i++) {
            new Thread("執行緒" + i) {
                public void run() {
                    while (true) {
                        if (tickets.isEmpty()) break;
                        System.out.println(Thread.currentThread().getName() + "---" + tickets.remove(0));
                    }
                }
            }.start();
        }
    }
}

 

二.併發類容器

jdk5.0以後提供了多種併發類容器來替代同步類容器從而改善效能,併發類容器時專門針對併發設計的:使用ConcurrentHashMap來代替基於雜湊的Hashtable,並添加了一些常見符合操作的支援;使用CopyOnWriteArrayList代替Vector,併發的CopyOnWriteArraySet,以及併發的Queue:ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高效能的佇列,後者是以阻塞形式的佇列。具體形式的Queue還有很多如:ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

2.1 ConcurrentMap

ConcurrentMap介面有兩個重要的實現:ConcurrentHashMap和ConcurrentSkipListMap(支援併發排序的功能,彌補ConcurrentHashMap的不足)。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的Hashtable,他們有自己的鎖,只要多個修改操作發生在不同的段上,它們就可以併發執行。ConcurrentHashMap把一個整體分成了16個段,也就是最高支援16個執行緒的併發修改操作,這也是在多執行緒場景下減小鎖的粒度從而降低鎖競爭的一種方案,同時代碼中大多數的共享變數使用volatile關鍵字宣告,目的是第一時間獲取修改的內容,效能非常好。

public class UseConcurrentMap {

    public static void main(String[] args) {
        ConcurrentHashMap<String, Object> chm = new ConcurrentHashMap<String, Object>();
        chm.put("k1", "v1");
        chm.put("k2", "v2");
        chm.put("k3", "v3");
        chm.putIfAbsent("k3", "vvvv");
      

        for (Map.Entry<String, Object> me : chm.entrySet()) {
            System.out.println("key:" + me.getKey() + ",value:" + me.getValue());
        }
    }
}

輸出結果為:

key:k1,value:v1
key:k2,value:v2
key:k3,value:v3

 在上面的結果中,k3的輸出值是v3,並不是vvvv。putIfAbsent方法表示,如果當前key已經有值了,則不再添加當前元素了,否則會新增。

2.2 Copy-On-Write容器

copy-on-write簡稱COW,是一種用於程式設計中的優化策略。JDK中的COW容器有兩種,CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常的有用,可以在非常多的併發場景中使用到。

什麼是CopyOnWrite容器?

CopyOnWrite容器即寫時複製的容器,通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後往新的容器裡新增元素。元素新增完成之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何的元素,所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器,非常適合讀多寫少的情況。CopyOnWriteArrayList讀寫分離的核心程式碼如下:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

CopyOnWriteArraySet新增元素的實現程式碼如下:

 /**
     * A version of addIfAbsent using the strong hint that given
     * recent snapshot does not contain e.
     */
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }