Lock介面、重入鎖ReentrantLock、讀寫鎖ReentrantReadWriteLock

z1340954953發表於2018-05-02

Lock特點

Lock介面提供了和synchronized關鍵字類似的同步功能,synchronized關鍵字只是隱式低獲取鎖,而lock擁有了鎖獲取和釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等同步特性

Lock使用

Lock lock = new ReentrantLock();
lock.lock();
try{
			
}finally{
  lock.unlock();
}

在finally塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放

不要將獲取鎖的過程中寫在try塊中,因為如果在獲取鎖(自定義鎖的實現)時候發生了異常,異常丟擲的同時,也會導致鎖無故釋放

Lock介面提供的synchronized關鍵字不具備的特性
特性    描述
1 .嘗試非阻塞地獲取鎖當前執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取鎖,比如synchronized如果一個執行緒獲取到鎖,另一個執行緒要獲取到鎖,就必須阻塞等待,而lock不用
2. 能夠被中斷的獲取鎖和synchronized不同,獲取到鎖的執行緒能夠響應中斷,當獲取到lock鎖的執行緒被中斷,將丟擲異常,並且釋放鎖
3.支援超時獲取鎖lock在指定的時間去獲取鎖,如果截止時間到了,仍然無法獲取鎖,則返回
Lock介面的API

重入鎖ReentrantLock

能夠支援一個執行緒對資源的重複加鎖,這點synchronized也能夠做到

public class GetLockAgain {
	private Lock lock1 = new ReentrantLock();
	private Lock lock2 = new ReentrantLock();
	public void test (){
		try{
			lock1.lock();
			System.out.println("獲取到lock1");
			lock2.lock();
			System.out.println("獲取到lock2");
		}finally{
			lock1.unlock();
			lock2.unlock();
		}
	}
	public static void main(String[] args) {
		final GetLockAgain gla = new GetLockAgain();
		gla.test();
	}
}

輸出:

獲取到lock1
獲取到lock2

ReentrantLock支援公平和非公平選擇

什麼是鎖的公平性?

在時間上,先請求鎖的執行緒,能夠先獲取到鎖,就是公平鎖,反之,就是非公平鎖

ReentrantLock提供了建構函式控制:


boolean fair : true ,公平鎖,false ,非公平鎖,預設的構造是非公平鎖

讀寫鎖

之前提到的鎖都是排他鎖,這些鎖在一個時刻只能被一個執行緒佔用。讀寫鎖維護了一堆鎖,讀鎖和寫鎖,讀鎖在同一個時刻

可以允許多個執行緒訪問,寫鎖只能允許一個執行緒訪問,   寫鎖是排他鎖,讀鎖是共享鎖

實際開發中,讀寫鎖的效能比排他鎖效能更好,因為很多場景下都是讀執行緒,另外可以設定讀寫鎖的公平性並且支援鎖重入


* 寫鎖的獲取

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

原始碼註釋是當前執行緒持有的讀鎖數量不為0 (也就是獲取到了讀鎖就不能在去獲取寫鎖)或者 已經持有寫鎖的執行緒不是當前執行緒,寫鎖會獲取失敗,否則能夠再次獲取

* 讀鎖的獲取

protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }
如果如果當前執行緒獲取到了讀鎖(CAS更新count)或者沒有獲取過,當前執行緒獲取到讀鎖,並且有如果寫鎖被其他執行緒佔用了,讀鎖會獲取失敗

例如,將讀寫鎖應用在快取中

/**
 * 讀寫鎖應用於快取
 * @author zhouy
 *
 */
public class Cache {
	private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private static Lock r = lock.readLock();
	private static Lock w = lock.writeLock();
	private static Map<String, String> map = new HashMap<>();
	/**
	 * 向快取中寫入,多個執行緒需要排隊執行
	 * @param key
	 * @param data
	 */
	public final static void put(String key,String data){
		try{
			w.lock();
			map.put(key, data);
		}finally{
			w.unlock();
		}
	}
	/**
	 * 讀取緩衝內容,多個執行緒可以同時訪問
	 * @param key
	 * @return
	 */
	public final static String read(String key){
		try{
			r.lock();
			return map.get(key);
		}finally{
			r.unlock();
		}
	}
	/**
	 * 清空快取
	 */
	public final static void clear(){
		try{
			w.lock();
			map.clear();
		}finally{
			w.unlock();
		}
	}
}

讀寫鎖的鎖降級

鎖升級:從讀鎖升級到寫鎖 

鎖降級:  從寫鎖降級到讀鎖,指的是先持有寫鎖,再去獲取讀鎖,隨後釋放之前持有的寫鎖的過程

ReentrantReadWriteLock 支援鎖降級(獲取到寫鎖,再去獲取讀鎖),不支援鎖升級(前面的原始碼可以看出,獲取到讀鎖是無法再去獲取寫鎖的)

鎖升級死鎖

public class ReadWriteLock {
	private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
	private Lock w = rw.writeLock();
	private Lock r = rw.readLock();
	public void test(){
		try{
			r.lock();
			System.out.println("獲取到讀鎖");
			w.lock();
			System.out.println("獲取到寫鎖");
			w.unlock();
		}finally{
			r.unlock();
		}
		
	}
	public static void main(String[] args) {
		ReadWriteLock rwl = new ReadWriteLock();
		rwl.test();
	}
}

輸出:


鎖降級

public class ReadWriteLock {
	private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
	private Lock w = rw.writeLock();
	private Lock r = rw.readLock();
	public void test(){
		try{
			w.lock();
			System.out.println("獲取到寫鎖");
			r.lock();
			System.out.println("獲取到讀鎖");
			r.unlock();
		}finally{
			w.unlock();
		}
		
	}
	public static void main(String[] args) {
		ReadWriteLock rwl = new ReadWriteLock();
		rwl.test();
	}
}

輸出:


為什麼使用到鎖降級?

在多執行緒環境中,如果存在一個執行緒在修改一個共享變數前需要先去讀取這個變數的值,如果是先獲取寫鎖,阻塞其他執行緒,在去讀取這個共享變數的值,就能夠保證資料的正確

反之,如果使用讀鎖先讀取,在釋放去獲取寫鎖,更新這個值,這個值可能已經被其他執行緒改過了。


相關文章