鎖在並發編程中的重要性不言而喻, 但是如何更好地選擇, 下面借幾個問答來開始吧! 後續我會再寫一篇有關於無鎖隊列的Blog
談到這個問題, 主要先從這幾個方面來入手:
- 線程的幾種狀態
- synchonrize的幾種使用方法比較
- synchonrize和volatile比較
- synchonrize和juc中的鎖比較
- 用了鎖就真的沒有並發問題了麼?
不熟悉線程的生命周期和相互的轉換控制, 是無法寫好並發代碼的.
圖簡單易懂, 主要是搞清楚, sleep, yield, wait, notify, notifyAll對於鎖的處理, 這裡就不多展開了. 簡單比較如下:
wait有出讓Object鎖的語義, 要想出讓鎖, 前提是要先獲得鎖, 所以要先用synchronized獲得鎖之後才能調用wait. notify原因類似, Object.wait()和notify()不具有原子性語義, 所以必須用synchronized保證線程安全.
yield()方法對應了如下操作: 先檢測當前是否有相同優先級的線程處於同可運行狀態, 如有, 則把 CPU 的占有權交給此線程, 否則繼續運行原來的線程. 所以yield()方法稱為“退讓”, 它把運行機會讓給了同等優先級的其他線程.
synchronize關鍵字主要有下面5種用法
public class SyncMethod {
private int value = 0;
private final Object mutex = new Object();
public synchronized int incAndGet0() {
return ++value;
}
public int incAndGet1() {
synchronized(this){
return ++value;
}
}
public int incAndGet2() {
synchronized(SyncMethod.class){
return ++value;
}
}
public int incAndGet3() {
synchronized(mutex){
return ++value;
}
}
public static synchonrize int incAndGet4() {
synchronized(mutex){
return ++value;
}
}
}
現在來分析:
sychronized的對象最好選擇引用不會變化的對象(例如被標記為final,或初始化後永遠不會變), 雖然synchronized是在對象上加鎖, 但是它首先要通過引用來定位對象, 如果引用會變化, 可能帶來意想不到的後果
簡單的說就是synchronized的代碼塊是確保可見性和原子性的, volatile只能確保可見性 當且僅當下面條件全部滿足時, 才能使用volatile
- 對變量的寫入操作不依賴於變量的當前值, (++i/i++這種肯定不行), 或者能確保只有單個線程在更新
- 該變量不會與其他狀態變量一起納入不變性條件中
- 訪問變量時不需要加鎖
ReentrantLock在內存上的語義於synchronize相同, 但是它提供了額外的功能, 可以作為一種高級工具. 當需要一些 可定時, 可輪詢, 可中斷的鎖獲取操作, 或者希望使用公平鎖, 或者使用非塊結構的編碼時 才應該考慮ReetrantLock.
總結一點, 在業務並發簡單清晰的情況下推薦synchronized, 在業務邏輯並發復雜, 或對使用鎖的擴展性要求較高時, 推薦使用ReentrantLock這類鎖. 另外今後JVM的優化方向一定是基於底層synchronize的, 性能方面應該選擇synchronize
先上代碼, 看一下是否有並發問題
Map syncMap = Collections.synchronizedMap(new HashMap());
if(!map.containsKey("a")){
map.put("a",value);
}
雖然Map上所有的方法都已被synchronize保護了, 但是在外部使用的時候, 一定要注意競態條件
競態條件: 先檢查後執行的這種操作是最常見的競態條件
下面是並發條件下的一些Donts
- Don’t synchronize on an object you’re changing
- Don’t synchronize on a String literal
- Don’t synchronize on auto-boxed values
- Don’t synchronize on null
- Don’t synchronize on a Lock object
- Don’t synchronize on getClass()
- Be careful locking on a thread-safe object with encapsulated locking
juc中的鎖分兩種, 1. 可重入鎖; 2. 讀寫鎖. 兩者都用到了一個通用組件 AbstractQueuedSynchronizer. 先從它說起
利用了一個int來表示狀態, 內部基於FIFO隊列及UnSafe的CAS原語作為操縱狀態的數據結構, AQS以單個 int 類型的原子變量來表示其狀態,定義了4個抽象方法( tryAcquire(int)、tryRelease(int)、tryAcquireShared(int)、tryReleaseShared(int),前兩個方法用於獨占/排他模式,後兩個用於共享模式 )留給子類實現,用於自定義同步器的行為以實現特定的功能。這方面的介紹大家看一下資料2, 描述非常清楚
引用資料2中的一段話:
同步器是實現鎖的關鍵,利用同步器將鎖的語義實現,然後在鎖的實現中聚合同步器。可以這樣理解:鎖的API是面向使用者的,它定義了與鎖交互的公共行為,而每個鎖需要完成特定的操作也是透過這些行為來完成的(比如:可以允許兩個線程進行加鎖,排除兩個以上的線程),但是實現是依托給同步器來完成;同步器面向的是線程訪問和資源控制,它定義了線程對資源是否能夠獲取以及線程的排隊等操作。鎖和同步器很好的隔離了二者所需要關注的領域,嚴格意義上講,同步器可以適用於除了鎖以外的其他同步設施上(包括鎖)。
可重入鎖, 支持公平和非公平策略(FairSync/NonFairSync), 默認非公平鎖, 內部Sync繼承於AbstractQueuedSynchronizer.
兩者代碼區別是:
FairSync 代碼中對於嘗試加鎖時(tryAcquire)多了一個判斷方法, 判斷等待隊列中是否還有比當前線程更早的, 如果為空,或者當前線程線程是等待隊列的第一個時才占有鎖
if (c == 0) {
if (!hasQueuedPredecessors() && //就是這裡
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
可重入的讀寫鎖, 首先我想到的是它的適用場景, 它與volatile有何區別, 又有何優勢呢?
volatile只能保證可見性, 在1寫N讀的情況下, 使用它就足夠了. 但是如何N寫N讀, 如何保證數據一致性而又減少並行度的損失呢? 就要看ReentrantReadWriteLock了.
讀鎖
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);//共享鎖
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
寫鎖
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);//獨占鎖
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
WriteLock就是一個獨占鎖,這和ReentrantLock裡面的實現幾乎相同,都是使用了AQS的acquire/release操作。當然了在內部處理方式上與ReentrantLock還是有一點不同的。對比清單1和清單2可以看到,ReadLock獲取的是共享鎖,WriteLock獲取的是獨占鎖。
AQS中有一個state字段(int類型,32位)用來描述有多少線程獲持有鎖。在獨占鎖的時代這個值通常是0或者1(如果是重入的就是重入的次數),在共享鎖的時代就是持有鎖的數量。在上一節中談到,ReadWriteLock的讀、寫鎖是相關但是又不一致的,所以需要兩個數來描述讀鎖(共享鎖)和寫鎖(獨占鎖)的數量。顯然現在一個state就不夠用了。於是在ReentrantReadWrilteLock裡面將這個字段一分為二,高位16位表示共享鎖的數量,低位16位表示獨占鎖的數量(或者重入數量)。2^16-1=65536,所以共享鎖和獨占鎖的數量最大只能是65535。
持有鎖線程數非0(c=getState()不為0),如果寫線程數(w)為0(那麼讀線程數就不為0)或者獨占鎖線程(持有鎖的線程)不是當前線程就返回失敗,或者寫入鎖的數量(其實是重入數)大於65535就拋出一個Error異常
如果當且寫線程數位0(那麼讀線程也應該為0,因為步驟1已經處理c!=0的情況),並且當前線程需要阻塞那麼就返回失敗;如果增加寫線程數失敗也返回失敗
設置獨占線程(寫線程)為當前線程,返回true。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if ((w == 0 && writerShouldBlock(current)) ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
重入性
讀寫鎖允許讀線程和寫線程按照請求鎖的順序重新獲取讀取鎖或者寫入鎖。當然了只有寫線程釋放了鎖,讀線程才能獲取重入鎖。 寫線程獲取寫入鎖後可以再次獲取讀取鎖,但是讀線程獲取讀取鎖後卻不能獲取寫入鎖。 另外讀寫鎖最多支持65535個遞歸寫入鎖和65535個遞歸讀取鎖。
鎖降級
寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
鎖升級
讀取鎖是不能直接升級為寫入鎖的。因為獲取一個寫入鎖需要釋放所有讀取鎖,所以如果有兩個讀取鎖視圖獲取寫入鎖而都不釋放讀取鎖時就會發生死鎖。
鎖獲取中斷
讀取鎖和寫入鎖都支持獲取鎖期間被中斷。這個和獨占鎖一致。
條件變量
寫入鎖提供了條件變量(Condition)的支持,這個和獨占鎖一致,但是讀取鎖卻不允許獲取條件變量,將得到一個UnsupportedOperationException異常。