從這一節開始介紹鎖里面的最后一個工具:讀寫鎖(ReadWriteLock)。
ReentrantLock 實現了標準的互斥操作,也就是一次只能有一個線程持有鎖,也即所謂獨占鎖的概念。前面的章節中一直在強調這個特點。顯然這個特點在一定程度上面減低了吞吐量,實際上獨占鎖是一種保守的鎖策略,在這種情況下任何“讀/讀”,“寫/讀”,“寫/寫”操作都不能同時發生。但是同樣需要強調的一個概念是,鎖是有一定的開銷的,當并發比較大的時候,鎖的開銷就比較客觀了。所以如果可能的話就盡量少用鎖,非要用鎖的話就嘗試看能否改造為讀寫鎖。
ReadWriteLock描述的是:一個資源能夠被多個讀線程訪問,或者被一個寫線程訪問,但是不能同時存在讀寫線程。也就是說讀寫鎖使用的場合是一個共享資源被大量讀取操作,而只有少量的寫操作(修改數據)。清單1描述了ReadWriteLock的API。
清單1 ReadWriteLock 接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
清單1描述的ReadWriteLock結構,這里需要說明的是ReadWriteLock并不是Lock的子接口,只不過ReadWriteLock借助Lock來實現讀寫兩個視角。在ReadWriteLock中每次讀取共享數據就需要讀取鎖,當需要修改共享數據時就需要寫入鎖。看起來好像是兩個鎖,但其實不盡然,在下一節中的分析中會解釋這點奧秘。
在JDK 6里面ReadWriteLock的實現是ReentrantReadWriteLock。
清單2 SimpleConcurrentMap
package xylz.study.concurrency.lock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SimpleConcurrentMap<K, V> implements Map<K, V> {
final ReadWriteLock lock = new ReentrantReadWriteLock();
final Lock r = lock.readLock();
final Lock w = lock.writeLock();
final Map<K, V> map;
public SimpleConcurrentMap(Map<K, V> map) {
this.map = map;
if (map == null) throw new NullPointerException();
}
public void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
public boolean containsKey(Object key) {
r.lock();
try {
return map.containsKey(key);
} finally {
r.unlock();
}
}
public boolean containsValue(Object value) {
r.lock();
try {
return map.containsValue(value);
} finally {
r.unlock();
}
}
public Set<java.util.Map.Entry<K, V>> entrySet() {
throw new UnsupportedOperationException();
}
public V get(Object key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
public boolean isEmpty() {
r.lock();
try {
return map.isEmpty();
} finally {
r.unlock();
}
}
public Set<K> keySet() {
r.lock();
try {
return new HashSet<K>(map.keySet());
} finally {
r.unlock();
}
}
public V put(K key, V value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
public void putAll(Map<? extends K, ? extends V> m) {
w.lock();
try {
map.putAll(m);
} finally {
w.unlock();
}
}
public V remove(Object key) {
w.lock();
try {
return map.remove(key);
} finally {
w.unlock();
}
}
public int size() {
r.lock();
try {
return map.size();
} finally {
r.unlock();
}
}
public Collection<V> values() {
r.lock();
try {
return new ArrayList<V>(map.values());
} finally {
r.unlock();
}
}
}
清單2描述的是用讀寫鎖實現的一個線程安全的Map。其中需要特別說明的是并沒有實現entrySet()方法,這是因為實現這個方法比較復雜,在后面章節中講到ConcurrentHashMap的時候會具體談這些細節。另外這里keySet()和values()也沒有直接返回Map的視圖,而是一個映射原有元素的新視圖,其實這個entrySet()一樣,是為了保護原始Map的數據邏輯,防止不正確的修改導致原始Map發生數據錯誤。特別說明的是在沒有特別需求的情況下沒有必要按照清單2寫一個線程安全的Map實現,因為ConcurrentHashMap已經完成了此操作。
ReadWriteLock需要嚴格區分讀寫操作,如果讀操作使用了寫入鎖,那么降低讀操作的吞吐量,如果寫操作使用了讀取鎖,那么就可能發生數據錯誤。
另外ReentrantReadWriteLock還有以下幾個特性:
- 公平性
- 非公平鎖(默認) 這個和獨占鎖的非公平性一樣,由于讀線程之間沒有鎖競爭,所以讀操作沒有公平性和非公平性,寫操作時,由于寫操作可能立即獲取到鎖,所以會推遲一個或多個讀操作或者寫操作。因此非公平鎖的吞吐量要高于公平鎖。
- 公平鎖 利用AQS的CLH隊列,釋放當前保持的鎖(讀鎖或者寫鎖)時,優先為等待時間最長的那個寫線程分配寫入鎖,當前前提是寫線程的等待時間要比所有讀線程的等待時間要長。同樣一個線程持有寫入鎖或者有一個寫線程已經在等待了,那么試圖獲取公平鎖的(非重入)所有線程(包括讀寫線程)都將被阻塞,直到最先的寫線程釋放鎖。如果讀線程的等待時間比寫線程的等待時間還有長,那么一旦上一個寫線程釋放鎖,這一組讀線程將獲取鎖。
- 重入性
- 讀寫鎖允許讀線程和寫線程按照請求鎖的順序重新獲取讀取鎖或者寫入鎖。當然了只有寫線程釋放了鎖,讀線程才能獲取重入鎖。
- 寫線程獲取寫入鎖后可以再次獲取讀取鎖,但是讀線程獲取讀取鎖后卻不能獲取寫入鎖。
- 另外讀寫鎖最多支持65535個遞歸寫入鎖和65535個遞歸讀取鎖。
- 鎖降級
- 寫線程獲取寫入鎖后可以獲取讀取鎖,然后釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
- 鎖升級
- 讀取鎖是不能直接升級為寫入鎖的。因為獲取一個寫入鎖需要釋放所有讀取鎖,所以如果有兩個讀取鎖視圖獲取寫入鎖而都不釋放讀取鎖時就會發生死鎖。
- 鎖獲取中斷
- 讀取鎖和寫入鎖都支持獲取鎖期間被中斷。這個和獨占鎖一致。
- 條件變量
- 寫入鎖提供了條件變量(Condition)的支持,這個和獨占鎖一致,但是讀取鎖卻不允許獲取條件變量,將得到一個
UnsupportedOperationException
異常。
- 重入數
- 讀取鎖和寫入鎖的數量最大分別只能是65535(包括重入數)。這在下節中有介紹。
上面幾個特性對讀寫鎖的理解很有幫助,而且也是必要的,另外在下一節中講ReadWriteLock的實現會用到這些知識的。
©2009-2014 IMXYLZ
|求賢若渴