從鎖的思想到Java主流鎖分析

樂觀鎖和悲觀鎖

樂觀鎖和悲觀鎖是一種概念,他們的區別主要是在對待線程同步時的態度。java

  • 悲觀鎖認爲本身在使用數據時必定存在其餘線程在修改數據,因此它在使用數據前會先加上鎖,待到使用完畢釋放鎖資源。Java中,synchronized關鍵字和Lock的實現類都屬於悲觀鎖。
  • 反之樂觀鎖則認爲在使用數據時不會有線程修改數據,因此它不會添加鎖,只是在更新數據時判斷是否有線程修改了數據。若是數據沒有被更新,則當前線程成功將數據寫入。若是數據被更新了,則會根據實現方式不一樣執行不一樣的處理(報錯 or 重試)。Java中,最多見的樂觀鎖實現就是CAS原子類。

正是由於樂觀鎖和悲觀鎖的不一樣,他們所適用的場景天然不同,算法

  1. 樂觀鎖適合於讀多寫少的場景,無鎖的設計能大幅提升併發效率。
  2. 悲觀鎖則適合寫多讀少場景,使用前先加鎖能保證數據安全。

使用

//=============== 悲觀鎖 ===============
//synchronized
public synchronized void test() {
    //須要同步的資源
}

/** * ReentrantLock * 須要保證多線程操做的是同一個鎖 */
ReentrantLock lock = new ReentrantLock();
public void test1() {
    lock.lock();
    try {
        //須要同步的資源
    } finally {
        lock.unlock();
    }
}

//=============== 樂觀鎖 ===============
/** * 須要保證多線程操做的是同一個AtomicInteger */
AtomicInteger atomicInteger = new AtomicInteger(0);
public void test2() {
    atomicInteger.getAndIncrement();
}
複製代碼

經過上述使用方式咱們能夠總結出,悲觀鎖都是經過顯式調用去獲取鎖從而同步數據,可是爲何樂觀鎖不須要顯示的獲取鎖也一樣能同步數據呢。這裏就要談談什麼是CAS。安全

CAS (compare and swap)

從字面意思上看,即比較和交換,是一種無鎖的算法。便可以在不須要加鎖的狀況下,實現多線程變量同步。在Java中,atomic包下的原子類們是CAS的一系列實現多線程

其中,咱們就以最多見的AtomicInteger分析,源碼以下,併發

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            //反射獲取AtomicInteger類中value值的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //經過volatile關鍵字防止cpu指令重排序
    //使value對全部線程可見
    private volatile int value;
    
    ...
    
    public final int getAndIncrement() {
        //實際調用的是Unsafe.getAndAddInt
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

//Unsafe類
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    //經過循環重試比較新值與舊值,直到二者相等說明此時數據未被其餘線程修改,以後更新內存中的變量值
    do {
        var5 = this.getIntVolatile(var1, var2);
    //compareAndSwapInt這個方法是native方法具體分析見底下
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

//native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
複製代碼

可見實際的CAS操做的實現是在native層的compareAndSwapInt()中,JNI裏是藉助於CPU指令cmpxchg完成的,該指令是一個原子操做。顯然,能夠保證變量的可見性。高併發

具體CPU的cmpxchg指令作的事情是,比較寄存器中的A和內存中的值V。ui

  1. 若是相等,把要寫入的新值 B 存入內存中。
  2. 若是不相等,將內存值 V 賦值給寄存器中的值 A。

以後經過上述的do-while循環再次調用cmpxchg指令進行重試,一直到更新成功爲止。this

CAS帶來的問題

CAS這種算法雖然很是高效,但也存在問題。atom

  1. ABA問題,由於CAS在更新變量前須要先檢查變量是否能夠更新,此時若是將變量A更新成B隨後立馬又更新成A。那麼顯然存在一種狀況致使CAS認爲變量沒有變化,但實際是有變化的(線程安全策略變得不可靠)。解決辦法能夠將變量每次的更新記錄一個版本號,即1A-2B-3A,這樣CAS作compare的時候就不會出現變量已更新卻被誤判爲未更新的狀況了。
  2. 循環策略致使CPU開銷高。

公平鎖和非公平鎖

  • 公平鎖,線程按照申請鎖的順序來持有鎖。優勢是等待的線程不會飢餓,但缺點是吞吐效率比非公平鎖降低。除了獲取鎖的線程,其他線程處於阻塞狀態,並且CPU作線程喚醒的開銷很大。
  • 非公平鎖,線程獲取鎖是無序的,存在線程插隊獲取到鎖的狀況。優勢是吞吐效率高,由於線程有概率不被阻塞就獲取到了鎖,但缺點可能會致使線程一直等待,處於飢餓狀態。

ReentrantLock中的公平鎖與非公平鎖

public class ReentrantLock implements Lock, java.io.Serializable {
    ...
    public ReentrantLock() {
        //可見ReentrantLock默認使用的是非公平鎖
        sync = new NonfairSync();
    }
    ...
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    //非公平鎖的實現
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
    
        final void lock() {
            ...
        }
    
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    //公平鎖的實現
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            ...
        }
        
        protected final boolean tryAcquire(int acquires) {
            ...
        }
    }
}
複製代碼

咱們觀察到實際獲取鎖的邏輯在tryAcquire方法中,咱們對NonfairSyncFairSync中(左爲FairSync)該方法作橫向比較來看看他們的區別是什麼,spa

除了增長了hasQueuedPredecessors之外沒有什麼不一樣,

public final boolean hasQueuedPredecessors() {
    ...
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
複製代碼

該方法主要是判斷當前線程是否位於同步隊列中的第一個。若是是則返回true,不然返回false。

可重入鎖和不可重入鎖

  • 可重入鎖,在外層方法獲取了鎖後,若是內部調用的方法也須要獲取鎖,那麼會自動獲取(必須爲同一個鎖)。不會由於外層方法獲取到的鎖沒有被釋放掉而被阻塞。可重入鎖的特色就是能夠在必定程度上避免產生死鎖。
  • 同理不可重入鎖則不容許出現上述狀況,好比不能使用它作遞歸操做。

獨享鎖和共享鎖

  • 獨享鎖又名互斥鎖。該鎖一次只能被一個線程所持有,得到鎖的線程能同時進行讀寫操做。Java中synchronizedLock的實現類都屬於互斥鎖。
  • 共享鎖,該鎖能夠被多個線程持有,若是一個變量A被線程加了共享鎖,則以後的線程也只能加共享鎖。而且得到共享鎖的線程只能寫,不能讀。

Java中ReentrantReadWriteLock類實現了互斥鎖與共享鎖,以下

ReentrantReadWriteLock有兩把鎖,ReadLock讀鎖,是共享鎖,WriteLock寫鎖,是互斥鎖。

相關文章
相關標籤/搜索