併發編程系列(十)—深入理解共享鎖和ReentrantReadWriteLock

在這裏插入圖片描述

前言

本文對Java的“共享鎖”進行介紹,JUC中的共享鎖有CountDownLatch, CyclicBarrier, Semaphore, ReentrantReadWriteLock等;本章會以ReentrantReadWriteLock爲例對共享鎖進行說明。內容包括

  • ReadWriteLock 和 ReentrantReadWriteLock介紹
  • ReadWriteLock 和 ReentrantReadWriteLock函數說明
  • ReentrantReadWriteLock 類圖結構
  • ReentrantReadWriteLock原理
  • ReentrantReadWriteLock示例

ReadWriteLock 和 ReentrantReadWriteLock介紹

ReadWriteLock維護了一對相關的鎖 — — “讀取鎖”和“寫入鎖”,一個用於讀取操作,另一個用於寫入操作。
“讀取鎖”用於只讀操作,它是“共享鎖”,能同時被多個線程獲取。
“寫入鎖”用於寫入操作,它是“獨佔鎖”,寫入鎖只能被一個線程鎖獲取。
注意:不能同時存在讀取鎖和寫入鎖!

ReadWriteLock 和 ReentrantReadWriteLock函數說明

// 返回用於讀取操作的鎖。
Lock readLock()
// 返回用於寫入操作的鎖。
Lock writeLock()
// 創建一個新的 ReentrantReadWriteLock,默認是採用“非公平策略”。
ReentrantReadWriteLock()
// 創建一個新的 ReentrantReadWriteLock,fair是“公平策略”。fair爲true,意味着公平策略;否則,意味着非公平策略。
ReentrantReadWriteLock(boolean fair)
// 返回一個 collection,它包含可能正在等待獲取讀取鎖的線程。
protected Collection<Thread> getQueuedReaderThreads()
// 返回一個 collection,它包含可能正在等待獲取讀取或寫入鎖的線程。
protected Collection<Thread> getQueuedThreads()
// 返回一個 collection,它包含可能正在等待獲取寫入鎖的線程。
protected Collection<Thread> getQueuedWriterThreads()
// 返回用於讀取操作的鎖。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用於寫入操作的鎖。
ReentrantReadWriteLock.WriteLock writeLock()

ReentrantReadWriteLock 類圖結構

ReentrantReadWriteLock 類圖結構
從類圖結構看出:

  • ReentrantReadWriteLock實現了ReadWriteLock接口。ReadWriteLock是一個讀寫鎖的接口,提供了"獲取讀鎖的readLock()函數" 和 “獲取寫鎖的writeLock()函數”。
  • ReentrantReadWriteLock中包含:sync對象,讀鎖readerLock和寫鎖writerLock。讀鎖ReadLock和寫鎖WriteLock都實現了Lock接口。讀鎖ReadLock和寫鎖WriteLock中也都分別包含了"Sync對象",它們的Sync對象和ReentrantReadWriteLock的Sync對象 是一樣的,就是通過sync,讀鎖和寫鎖實現了對同一個對象的訪問。
  • 和"ReentrantLock"一樣,sync是Sync類型;而且,Sync也是一個繼承於AQS的抽象類。Sync也包括"公平鎖"FairSync和"非公平鎖"NonfairSync。sync對象是"FairSync"和"NonfairSync"中的一個,默認是"NonfairSync"。

共享鎖源碼說明

ReadLock源碼
public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    // ReentrantReadWriteLock的AQS對象
    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 String toString() {
        int r = sync.getReadLockCount();
        return super.toString() +
            "[Read locks = " + r + "]";
    }
}

下面,分別從“獲取共享鎖”和“釋放共享鎖”兩個方面對共享鎖進行說明。

獲取鎖

獲取共享鎖的思想(即lock函數的步驟),是先通過tryAcquireShared()嘗試獲取共享鎖。嘗試成功的話,則直接返回;嘗試失敗的話,則通過doAcquireShared()不斷的循環並嘗試獲取鎖,若有需要,則阻塞等待。doAcquireShared()在循環中每次嘗試獲取鎖時,都是通過tryAcquireShared()來進行嘗試的。下面看看“獲取共享鎖”的詳細流程。

  • lock()
public void lock() {
    sync.acquireShared(1);
}
  • acquireShared()
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

說明:acquireShared()首先會通過tryAcquireShared()來嘗試獲取鎖。
嘗試成功的話,則不再做任何動作(因爲已經成功獲取到鎖了)。
嘗試失敗的話,則通過doAcquireShared()來獲取鎖。doAcquireShared()會獲取到鎖了才返回。

  • tryAcquireShared()
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    // 獲取“鎖”的狀態
    int c = getState();
    // 如果“鎖”是“互斥鎖”,並且獲取鎖的線程不是current線程;則返回-1。
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 獲取“讀取鎖”的共享計數
    int r = sharedCount(c);
    // 如果“不需要阻塞等待”,並且“讀取鎖”的共享計數小於MAX_COUNT;
    // 則通過CAS函數更新“鎖的狀態”,將“讀取鎖”的共享計數+1。
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 第1次獲取“讀取鎖”。
        if (r == 0) { 
            firstReader = current;
            firstReaderHoldCount = 1;
        // 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程
        } else if (firstReader == current) { 
            firstReaderHoldCount++;
        } else {
            // HoldCounter是用來統計該線程獲取“讀取鎖”的次數。
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            // 將該線程獲取“讀取鎖”的次數+1。
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

說明:tryAcquireShared()的作用是嘗試獲取“共享鎖”。
如果在嘗試獲取鎖時,“不需要阻塞等待”並且“讀取鎖的共享計數小於MAX_COUNT”,則直接通過CAS函數更新“讀取鎖的共享計數”,以及將“當前線程獲取讀取鎖的次數+1”。
否則,通過fullTryAcquireShared()獲取讀取鎖。

  • fullTryAcquireShared()
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        // 獲取“鎖”的狀態
        int c = getState();
        // 如果“鎖”是“互斥鎖”,並且獲取鎖的線程不是current線程;則返回-1。
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 如果“需要阻塞等待”。
        // (01) 當“需要阻塞等待”的線程是第1個獲取鎖的線程的話,則繼續往下執行。
        // (02) 當“需要阻塞等待”的線程獲取鎖的次數=0時,則返回-1。
        } else if (readerShouldBlock()) {
            // 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程
            if (firstReader == current) {
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId()) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 如果當前線程獲取鎖的計數=0,則返回-1。
                if (rh.count == 0)
                    return -1;
            }
        }
        // 如果“不需要阻塞等待”,則獲取“讀取鎖”的共享統計數;
        // 如果共享統計數超過MAX_COUNT,則拋出異常。
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 將線程獲取“讀取鎖”的次數+1。
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 如果是第1次獲取“讀取鎖”,則更新firstReader和firstReaderHoldCount。
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            // 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程,
            // 則將firstReaderHoldCount+1。
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                // 更新線程的獲取“讀取鎖”的共享計數
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

說明:fullTryAcquireShared()會根據“是否需要阻塞等待”,“讀取鎖的共享計數是否超過限制”等等進行處理。如果不需要阻塞等待,並且鎖的共享計數沒有超過限制,則通過CAS嘗試獲取鎖,並返回1。

  • doAcquireShared()
private void doAcquireShared(int arg) {
    // addWaiter(Node.SHARED)的作用是,創建“當前線程”對應的節點,並將該線程添加到CLH隊列中。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 獲取“node”的前一節點
            final Node p = node.predecessor();
            // 如果“當前線程”是CLH隊列的表頭,則嘗試獲取共享鎖。
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 如果“當前線程”不是CLH隊列的表頭,則通過shouldParkAfterFailedAcquire()判斷是否需要等待,
            // 需要的話,則通過parkAndCheckInterrupt()進行阻塞等待。若阻塞等待過程中,線程被中斷過,則設置interrupted爲true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

說明:doAcquireShared()的作用是獲取共享鎖。
它會首先創建線程對應的CLH隊列的節點,然後將該節點添加到CLH隊列中。CLH隊列是管理獲取鎖的等待線程的隊列。如果“當前線程”是CLH隊列的表頭,則嘗試獲取共享鎖;否則,則需要通過shouldParkAfterFailedAcquire()判斷是否阻塞等待,需要的話,則通過parkAndCheckInterrupt()進行阻塞等待。
doAcquireShared()會通過for循環,不斷的進行上面的操作;目的就是獲取共享鎖。需要注意的是:doAcquireShared()在每一次嘗試獲取鎖時,是通過tryAcquireShared()來執行的!

釋放鎖

釋放共享鎖的思想,是先通過tryReleaseShared()嘗試釋放共享鎖。嘗試成功的話,則通過doReleaseShared()喚醒“其他等待獲取共享鎖的線程”,並返回true;否則的話,返回flase。

  • unlock
public  void unlock() {
    sync.releaseShared(1);
}

說明:該函數實際上調用releaseShared(1)釋放共享鎖。

  • releaseShared()
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

說明:releaseShared()的目的是讓當前線程釋放它所持有的共享鎖。它首先會通過tryReleaseShared()去嘗試釋放共享鎖。嘗試成功,則直接返回;嘗試失敗,則通過doReleaseShared()去釋放共享鎖。

  • tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
    // 獲取當前線程,即釋放共享鎖的線程。
    Thread current = Thread.currentThread();
    // 如果想要釋放鎖的線程(current)是第1個獲取鎖(firstReader)的線程,
    // 並且“第1個獲取鎖的線程獲取鎖的次數”=1,則設置firstReader爲null;
    // 否則,將“第1個獲取鎖的線程的獲取次數”-1。
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    // 獲取rh對象,並更新“當前線程獲取鎖的信息”。
    } else {
 
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        // 獲取鎖的狀態
        int c = getState();
        // 將鎖的獲取次數-1。
        int nextc = c - SHARED_UNIT;
        // 通過CAS更新鎖的狀態。
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

說明:tryReleaseShared()的作用是嘗試釋放共享鎖。

  • doReleaseShared()
private void doReleaseShared() {
    for (;;) {
        // 獲取CLH隊列的頭節點
        Node h = head;
        // 如果頭節點不爲null,並且頭節點不等於tail節點。
        if (h != null && h != tail) {
            // 獲取頭節點對應的線程的狀態
            int ws = h.waitStatus;
            // 如果頭節點對應的線程是SIGNAL狀態,則意味着“頭節點的下一個節點所對應的線程”需要被unpark喚醒。
            if (ws == Node.SIGNAL) {
                // 設置“頭節點對應的線程狀態”爲空狀態。失敗的話,則繼續循環。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 喚醒“頭節點的下一個節點所對應的線程”。
                unparkSuccessor(h);
            }
            // 如果頭節點對應的線程是空狀態,則設置“文件點對應的線程所擁有的共享鎖”爲其它線程獲取鎖的空狀態。
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果頭節點發生變化,則繼續循環。否則,退出循環。
        if (h == head)                   // loop if head changed
            break;
    }
}

說明:doReleaseShared()會釋放“共享鎖”。它會從前往後的遍歷CLH隊列,依次“喚醒”然後“執行”隊列中每個節點對應的線程;最終的目的是讓這些線程釋放它們所持有的鎖。

公平共享鎖和非公平共享鎖

和互斥鎖ReentrantLock一樣,ReadLock也分爲公平鎖和非公平鎖。

公平鎖和非公平鎖的區別,體現在判斷是否需要阻塞的函數readerShouldBlock()是不同的

  • 在公平共享鎖中,如果在當前線程的前面有其他線程在等待獲取共享鎖,則返回true;否則,返回false。
  • 在非公平共享鎖中,它會無視當前線程的前面是否有其他線程在等待獲取共享鎖。只要該非公平共享鎖對應的線程不爲null,則返回true。

ReentrantReadWriteLock示例

public class RWLockTest {

	/**
	 * ReentrantReadWriterLock類提供兩把鎖,一把用於寫操作,一把用於讀操作,
	 * 通過在ReadWriterLock接口聲明的readLock獲取。
	 * 並可以使用lock(),unLock(),tryLock()對象
	 * 用於寫操作鎖,通過在ReadWriterLock接口聲明的writeLock獲取。
	 * 並可以使用lock(),unLock(),tryLock()方法,當對象獲得讀鎖時,不能去修改該對象
	 */
	public static void main(String[] args) {
		PrincePOJO prince=new PrincePOJO();
		//創建多個讀取線程對象
		Reader[] reader=new Reader[5];
		Thread[] threadsReader=new Thread[5];
		for(int i=0;i<5;i++){
			reader[i]=new Reader(prince);
			threadsReader[i]=new Thread(reader[i]);
		}
		//創建一個寫線程對象
		Thread threadWriter=new Thread(new Writer(prince));
		//啓動線程
		for(int i=0;i<5;i++){
			threadsReader[i].start();
		}
		threadWriter.start();
	}

	static class Writer implements Runnable {
		private PrincePOJO price;
		public Writer(PrincePOJO price){
			this.price=price;
		}
		//循環修改價格
		@Override
		public void run() {
			for(int i=0;i<1;i++){
				System.out.println(Thread.currentThread().getName()+":attempt to modify the price");
				price.setPrice(Math.random()*10 ,Math.random()*8);
				System.out.println(Thread.currentThread().getName()+":price have been modfied");
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	static class Reader implements Runnable {
		private PrincePOJO price ;
		public Reader(PrincePOJO price){
			this.price=price;
		}
		//讀取10次價格
		@Override
		public void run() {
			for(int i=0;i<3;i++){
				System.out.printf("%s: price1:%f-price2:%f\n",Thread.currentThread().getName(),price.getPrice1(),price.getPrice2());
			}
		}

	}

	static class PrincePOJO {
		private double price1;
		private double price2;
		//聲明一個ReadWriteLock對象
		private ReadWriteLock lock;

		public PrincePOJO(){
			this.price1=10.00;
			this.price2=8.00;
			lock=new ReentrantReadWriteLock();
		}
		//讀取price1
		public double getPrice1(){
			lock.readLock().lock();
			double value=price1;
			lock.readLock().unlock();
			return value;
		}
		//讀取price2
		public double getPrice2(){
			lock.readLock().lock();
			double value=price2;
			lock.readLock().unlock();
			return value;
		}
		//設置價格
		public void setPrice(double price1,double price2){
			lock.writeLock().lock();
			this.price1=price1;
			this.price2=price2;
			lock.writeLock().unlock();
		}
	}
}
運行結果
Thread-1: price1:10.000000-price2:8.000000
Thread-5:attempt to modify the price
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:4.354122-price2:3.533028
Thread-3: price1:10.000000-price2:8.000000
Thread-3: price1:4.354122-price2:3.533028
Thread-3: price1:4.354122-price2:3.533028
Thread-0: price1:10.000000-price2:8.000000
Thread-0: price1:4.354122-price2:3.533028
Thread-0: price1:4.354122-price2:3.533028
Thread-2: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-5:price have been modfied
Thread-1: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-1: price1:4.354122-price2:3.533028
分析說明
  • 觀察Thread-0和Thread-4的運行結果,我們發現,Thread-0啓動並獲取到“讀取鎖”,在它還沒運行完畢的時候,Thread-1,2,3,4也啓動了並且也成功獲取到“讀取鎖”。
    因此,“讀取鎖”支持被多個線程同時獲取。
  • 觀察Thread-5這三此“寫入鎖”的線程。只要“寫入鎖”被某線程獲取,則該線程運行完畢了,才釋放該鎖。
    因此,“寫入鎖”不支持被多個線程同時獲取。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章