歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

淺談Java中的鎖

鎖在並發編程中的重要性不言而喻, 但是如何更好地選擇, 下面借幾個問答來開始吧! 後續我會再寫一篇有關於無鎖隊列的Blog

1. synchonrize如何更好地使用?

談到這個問題, 主要先從這幾個方面來入手:

  1. 線程的幾種狀態
  2. synchonrize的幾種使用方法比較
  3. synchonrize和volatile比較
  4. synchonrize和juc中的鎖比較
  5. 用了鎖就真的沒有並發問題了麼?

1.1 線程的幾種狀態

不熟悉線程的生命周期和相互的轉換控制, 是無法寫好並發代碼的.

圖簡單易懂, 主要是搞清楚, sleep, yield, wait, notify, notifyAll對於鎖的處理, 這裡就不多展開了. 簡單比較如下:

方法是否釋放鎖備注 wait 是 wait和notify/notifyAll是成對出現的, 必須在synchronize塊中被調用 sleep 否 可使低優先級的線程獲得執行機會 yield 否 yield方法使當前線程讓出CPU占有權, 但讓出的時間是不可設定的

wait有出讓Object鎖的語義, 要想出讓鎖, 前提是要先獲得鎖, 所以要先用synchronized獲得鎖之後才能調用wait. notify原因類似, Object.wait()和notify()不具有原子性語義, 所以必須用synchronized保證線程安全.

yield()方法對應了如下操作: 先檢測當前是否有相同優先級的線程處於同可運行狀態, 如有, 則把 CPU 的占有權交給此線程, 否則繼續運行原來的線程. 所以yield()方法稱為“退讓”, 它把運行機會讓給了同等優先級的其他線程.

1.2 synchonrize的最佳實踐

synchronize關鍵字主要有下面5種用法

  1. 在方法上進行同步, 分為(1)instance method/(2)static method, 這兩個的區別後面說
  2. 在內部塊上進行同步, 分為(3)synchronize(this), (4)synchonrize(XXX.class), (5)synchonrize(mutex)
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;
        }
    }
}

現在來分析:

  1. 作為修飾符加在方法聲明上, synchronized修飾非靜態方法時表示鎖住了調用該方法的堆對象, 修飾靜態方法時表示鎖住了這個類在方法區中的類對象.
  2. synchronized(X.class) 使用類對象作為monitor. 同一時間只有一個線程可以能訪問塊中資源.
  3. synchronized(this)和synchronized(mutex) 都是對象鎖, 同一時間每個實例都保證只能有一個實例能訪問塊中資源.

sychronized的對象最好選擇引用不會變化的對象(例如被標記為final,或初始化後永遠不會變), 雖然synchronized是在對象上加鎖, 但是它首先要通過引用來定位對象, 如果引用會變化, 可能帶來意想不到的後果

1.3 synchronized和volatile比較

簡單的說就是synchronized的代碼塊是確保可見性和原子性的, volatile只能確保可見性 當且僅當下面條件全部滿足時, 才能使用volatile

  1. 對變量的寫入操作不依賴於變量的當前值, (++i/i++這種肯定不行), 或者能確保只有單個線程在更新
  2. 該變量不會與其他狀態變量一起納入不變性條件中
  3. 訪問變量時不需要加鎖

1.4 synchonrize和juc中的鎖比較

ReentrantLock在內存上的語義於synchronize相同, 但是它提供了額外的功能, 可以作為一種高級工具. 當需要一些 可定時, 可輪詢, 可中斷的鎖獲取操作, 或者希望使用公平鎖, 或者使用非塊結構的編碼時 才應該考慮ReetrantLock.

總結一點, 在業務並發簡單清晰的情況下推薦synchronized, 在業務邏輯並發復雜, 或對使用鎖的擴展性要求較高時, 推薦使用ReentrantLock這類鎖. 另外今後JVM的優化方向一定是基於底層synchronize的, 性能方面應該選擇synchronize

1.5 用了鎖就真的沒有並發問題了麼?

先上代碼, 看一下是否有並發問題

Map syncMap = Collections.synchronizedMap(new HashMap());
if(!map.containsKey("a")){
    map.put("a",value);
}

雖然Map上所有的方法都已被synchronize保護了, 但是在外部使用的時候, 一定要注意競態條件

競態條件: 先檢查後執行的這種操作是最常見的競態條件

下面是並發條件下的一些Donts

  1. Don’t synchronize on an object you’re changing
  2. Don’t synchronize on a String literal
  3. Don’t synchronize on auto-boxed values
  4. Don’t synchronize on null
  5. Don’t synchronize on a Lock object
  6. Don’t synchronize on getClass()
  7. Be careful locking on a thread-safe object with encapsulated locking

2. Juc中的同步輔助類

2.1 Semaphore 信號量是一類經典的同步工具. 信號量通常用來限制線程可以同時訪問的(物理或邏輯)資源數量.

2.2 CountDownLatch 一種非常簡單, 但很常用的同步輔助類. 其作用是在完成一組正在其他線程中執行的操作之前,允許一個或多個線程一直阻塞.

2.3 CyclicBarrier 一種可重置的多路同步點, 在某些並發編程場景很有用. 它允許一組線程互相等待, 直到到達某個公共的屏障點 (common barrier point). 在涉及一組固定大小的線程的程序中, 這些線程必須不時地互相等待, 此時 CyclicBarrier 很有用.因為該 barrier在釋放等待線程後可以重用, 所以稱它為循環的barrier.

2.4 Phaser 一種可重用的同步屏障, 功能上類似於CyclicBarrier和CountDownLatch, 但使用上更為靈活. 非常適用於在多線程環境下同步協調分階段計算任務(Fork/Join框架中的子任務之間需同步時, 優先使用Phaser)

2.5 Exchanger 允許兩個線程在某個匯合點交換對象, 在某些管道設計時比較有用. Exchanger提供了一個同步點, 在這個同步點, 一對線程可以交換數據. 每個線程通過exchange()方法的入口提供數據給他的伙伴線程, 並接收他的伙伴線程提供的數據並返回. 當兩個線程通過Exchanger交換了對象, 這個交換對於兩個線程來說都是安全的. Exchanger可以認為是 SynchronousQueue 的雙向形式, 在運用到遺傳算法和管道設計的應用中比較有用.


3. juc中的鎖源碼分析

juc中的鎖分兩種, 1. 可重入鎖; 2. 讀寫鎖. 兩者都用到了一個通用組件 AbstractQueuedSynchronizer. 先從它說起

3.1 AbstractQueuedSynchronizer

利用了一個int來表示狀態, 內部基於FIFO隊列及UnSafe的CAS原語作為操縱狀態的數據結構, AQS以單個 int 類型的原子變量來表示其狀態,定義了4個抽象方法( tryAcquire(int)、tryRelease(int)、tryAcquireShared(int)、tryReleaseShared(int),前兩個方法用於獨占/排他模式,後兩個用於共享模式 )留給子類實現,用於自定義同步器的行為以實現特定的功能。這方面的介紹大家看一下資料2, 描述非常清楚

引用資料2中的一段話:

同步器是實現鎖的關鍵,利用同步器將鎖的語義實現,然後在鎖的實現中聚合同步器。可以這樣理解:鎖的API是面向使用者的,它定義了與鎖交互的公共行為,而每個鎖需要完成特定的操作也是透過這些行為來完成的(比如:可以允許兩個線程進行加鎖,排除兩個以上的線程),但是實現是依托給同步器來完成;同步器面向的是線程訪問和資源控制,它定義了線程對資源是否能夠獲取以及線程的排隊等操作。鎖和同步器很好的隔離了二者所需要關注的領域,嚴格意義上講,同步器可以適用於除了鎖以外的其他同步設施上(包括鎖)。

3.2 ReentrantLock

可重入鎖, 支持公平和非公平策略(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());
}

3.3 ReentrantReadWriteLock

3.3.1 引子

可重入的讀寫鎖, 首先我想到的是它的適用場景, 它與volatile有何區別, 又有何優勢呢?

volatile只能保證可見性, 在1寫N讀的情況下, 使用它就足夠了. 但是如何N寫N讀, 如何保證數據一致性而又減少並行度的損失呢? 就要看ReentrantReadWriteLock了.

3.3.2 源碼分析:

讀鎖

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。

3.3.3 寫入鎖分析:
  1. 持有鎖線程數非0(c=getState()不為0),如果寫線程數(w)為0(那麼讀線程數就不為0)或者獨占鎖線程(持有鎖的線程)不是當前線程就返回失敗,或者寫入鎖的數量(其實是重入數)大於65535就拋出一個Error異常

  2. 如果當且寫線程數位0(那麼讀線程也應該為0,因為步驟1已經處理c!=0的情況),並且當前線程需要阻塞那麼就返回失敗;如果增加寫線程數失敗也返回失敗

  3. 設置獨占線程(寫線程)為當前線程,返回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;
}
3.3.4 列出讀寫鎖幾個特性:
  • 重入性

    讀寫鎖允許讀線程和寫線程按照請求鎖的順序重新獲取讀取鎖或者寫入鎖。當然了只有寫線程釋放了鎖,讀線程才能獲取重入鎖。 寫線程獲取寫入鎖後可以再次獲取讀取鎖,但是讀線程獲取讀取鎖後卻不能獲取寫入鎖。 另外讀寫鎖最多支持65535個遞歸寫入鎖和65535個遞歸讀取鎖。

  • 鎖降級

    寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

  • 鎖升級

    讀取鎖是不能直接升級為寫入鎖的。因為獲取一個寫入鎖需要釋放所有讀取鎖,所以如果有兩個讀取鎖視圖獲取寫入鎖而都不釋放讀取鎖時就會發生死鎖。

  • 鎖獲取中斷

    讀取鎖和寫入鎖都支持獲取鎖期間被中斷。這個和獨占鎖一致。

  • 條件變量

寫入鎖提供了條件變量(Condition)的支持,這個和獨占鎖一致,但是讀取鎖卻不允許獲取條件變量,將得到一個UnsupportedOperationException異常。

 

Copyright © Linux教程網 All Rights Reserved