1. 程式人生 > >018.多執行緒-悲觀鎖、樂觀鎖、重入鎖、讀寫鎖、自旋鎖、CAS無鎖機制

018.多執行緒-悲觀鎖、樂觀鎖、重入鎖、讀寫鎖、自旋鎖、CAS無鎖機制

悲觀鎖(Pessimistic Lock)

顧名思義,就是很悲觀。每次去拿資料的時候都認為別人會修改,所以都會上鎖。這樣別人想拿這個資料就會阻塞(block)直到它拿到鎖。傳統的關係型資料庫裡面就用到了很多這種鎖機制。比如:行鎖,表鎖,讀鎖,寫鎖等,都是在做操作之前先上鎖。


樂觀鎖(Optimistic Lock)

顧名思義,就是很樂觀。每次去拿資料的時候都認為別人不會修改,所以不會上鎖。但是在更新的時候會判斷一下,在此期間是否有人去更新這個資料,利用版本號等機制來控制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量。


重入鎖

重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。
在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖

非重入鎖

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify
(); } }

使用該鎖,內層函式獲取該鎖時,將會發生死鎖。

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

重入鎖

public class
Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread thread = Thread.currentThread(); while(isLocked && lockedBy != thread){ wait(); } isLocked = true; lockedCount++; lockedBy = thread; } public synchronized void unlock(){ if(Thread.currentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } }

此時,新增了鎖定執行緒和鎖定數量兩個引數,
同一個執行緒,遞迴獲取該鎖,不會發生死鎖。


讀寫鎖

多個執行緒可以同時去讀一個共享資源。
但是如果有一個執行緒在寫這個共享資源,
此時就不應該再有其它執行緒對該資源進行讀或寫。

讀寫鎖能夠保證讀取資料的 嚴格實時性,
如果不需要這種 嚴格實時性,那麼不需要加讀寫鎖。

public class Cache {
	static Map<String, Object> map = new HashMap<String, Object>();
	static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	static Lock r = rwl.readLock();
	static Lock w = rwl.writeLock();

	// 獲取一個key對應的value
	public static final Object get(String key) {
		r.lock();
		try {
			System.out.println("正在做讀的操作,key:" + key + " 開始");
			Thread.sleep(100);
			Object object = map.get(key);
			System.out.println("正在做讀的操作,key:" + key + " 結束");
			System.out.println();
			return object;
		} catch (InterruptedException e) {

		} finally {
			r.unlock();
		}
		return key;
	}

	// 設定key對應的value,並返回舊有的value
	public static final Object put(String key, Object value) {
		w.lock();
		try {

			System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "開始.");
			Thread.sleep(100);
			Object object = map.put(key, value);
			System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "結束.");
			System.out.println();
			return object;
		} catch (InterruptedException e) {

		} finally {
			w.unlock();
		}
		return value;
	}

	// 清空所有的內容
	public static final void clear() {
		w.lock();
		try {
			map.clear();
		} finally {
			w.unlock();
		}
	}

	public static void main(String[] args) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.put(i + "", i + "");
				}

			}
		}).start();
		new Thread(new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.get(i + "");
				}

			}
		}).start();
	}
}

CAS(compare and swap)無鎖機制

(1)與鎖相比,使用比較交換(下文簡稱CAS),由於其非阻塞性,它對死鎖問題天生免疫,並且,執行緒間的相互影響也遠遠比基於鎖的方式要小。更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有執行緒間頻繁排程帶來的開銷,因此,它要比基於鎖的方式擁有更優越的效能。


(2)無鎖的好處:
第一,在高併發的情況下,它比有鎖的程式擁有更好的效能;
第二,它天生就是死鎖免疫的。
就憑藉這兩個優勢,就值得我們冒險嘗試使用無鎖的併發。


(3)CAS演算法的過程是這樣:它包含三個引數CAS(V,E,N):
V表示要更新的變數,E表示預期值,N表示新值。
僅當V值等於E值時,才會將V的值設為N,
如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。
最後,CAS返回當前V的真實值。


(4)CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。


(5)簡單地說,CAS需要你額外給出一個期望值,也就是你認為這個變數現在應該是什麼樣子的。如果變數不是你想象的那樣,那說明它已經被別人修改過了。你就重新讀取,再次嘗試修改就好了。


(6)在硬體層面,大部分的現代處理器都已經支援原子化的CAS指令。在JDK 5.0以後,虛擬機器便可以使用這個指令來實現併發操作和併發資料結構,並且,這種操作在虛擬機器中可以說是無處不在。

	/** 
	 * Atomically increments by one the current value. 
	 * 
	 * @return the updated value 
	 */  
	public final int incrementAndGet() {  
	    for (;;) {  
	        //獲取當前值  
	        int current = get();  
	        //設定期望值  
	        int next = current + 1;  
	        //呼叫Native方法compareAndSet,執行CAS操作  
	        if (compareAndSet(current, next))  
	            //成功後才會返回期望值,否則無線迴圈  
	            return next;  
	    }  
	}  

自旋鎖(spinlock)

當一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,
然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈。

獲取鎖的執行緒一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖會造成busy-waiting。

它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,”自旋”一詞就是因此而得名。

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

lock()方法利用的CAS,當第一個執行緒A獲取鎖的時候,能夠成功獲取到,不會進入while迴圈,
如果此時執行緒A沒有釋放鎖,另一個執行緒B又來獲取鎖,此時由於不滿足CAS,所以就會進入while迴圈,
不斷判斷是否滿足CAS,直到A執行緒呼叫unlock方法釋放了該鎖。

由於自旋鎖只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。