同步類容器 & 併發類容器
目錄
一.同步類容器
同步類容器是執行緒安全的,但在某些場景下可能需要加鎖來保護複合操作,例如:迭代(反覆訪問元素,元素的遍歷)、跳轉(根據指定的順序找到當前元素的下一個元素)以及條件運算。這些複合操作在多執行緒併發的修改容器時,可能會表現出意外的行為,最經典的是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();
}
}