1. 程式人生 > >實戰java高併發程式設計之ReentrantReadWriteLoc原始碼分析

實戰java高併發程式設計之ReentrantReadWriteLoc原始碼分析

前面分析了併發工具類CountDownLatchCyclicBarrier,本文分享分析比較重要的ReentrantReadWriteLock。

使用場景

以前的同步方式需要對讀、寫操作進行同步,讀讀之間,讀寫之間,寫寫之間等;工程師們發現讀讀之間並不會影響資料的一致性,完全可以不用同步。為了解決讀讀之間不阻塞,讀寫鎖就誕生啦!寫寫和讀寫由於有寫操作,會影響到資料的一致性的,因此他們之間需要阻塞。下面舉了一個例子來說明ReentrantLock和ReadWriteLock之間的差別:

public class ReadWriteLockTest {
	
	private static ReentrantLock lock =
new ReentrantLock(); private static ReadWriteLock rwLock = new ReentrantReadWriteLock(); public static void main(String[] args) { // 讀執行緒1 Runnable readThread1 = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 開始時間:" + System.
currentTimeMillis()); // lock.lock(); rwLock.readLock().lock(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { // lock.unlock(); rwLock.readLock().unlock(); } System.out.println(Thread.currentThread().getName() + " 結束時間:"
+ System.currentTimeMillis()); } }; // 讀執行緒2 Runnable readThread2 = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 開始時間:" + System.currentTimeMillis()); // lock.lock(); rwLock.readLock().lock(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { // lock.unlock(); rwLock.readLock().unlock(); } System.out.println(Thread.currentThread().getName() + " 結束時間:" + System.currentTimeMillis()); } }; new Thread(readThread1, "readThread1").start(); new Thread(readThread2, "readThread2").start(); } }

執行結果:

ReentrantLock的結果	
readThread1 開始時間:1539692006271
readThread2 開始時間:1539692006271
readThread1 結束時間:1539692008271
readThread2 結束時間:1539692010271
ReentrantReadWriteLock的結果	
readThread1 開始時間:1539691790214
readThread2 開始時間:1539691790215
readThread1 結束時間:1539691792215
readThread2 結束時間:1539691792215

可見ReentrantLock鎖的使用時間是4秒,也就是兩個讀執行緒的和;ReentrantReadWriteLock鎖的使用時間是2秒,可見readThread1和readThread2兩執行緒是並行的。 在讀讀不影響的情況下,ReentrantReadWriteLock優於ReentrantLock;因此當專案中讀多於寫時適合用讀寫鎖。 那麼ReentrantReadWriteLock是如何實現讀寫鎖的呢?先來看一個讀寫鎖分離的例項。

例項程式碼

定義兩個讀執行緒和一個寫執行緒,啟動順序為:讀1-寫1-讀2;

public class ReentrantReadWriteLockTest {

	private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
	private static Lock readLock = rwLock.readLock();  // 讀鎖
	private static Lock writeLock = rwLock.writeLock();  // 寫鎖
	
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName() + "開始:" + System.currentTimeMillis());
				try {
					readLock.lock();
					Thread.sleep(1000);
					System.out.println(Thread.currentThread().getName() + "執行緒正在讀檔案...");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					readLock.unlock();
				}
				System.out.println(Thread.currentThread().getName() + "結束:" + System.currentTimeMillis());
			}
		}, "讀1").start();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName() + "開始:" + System.currentTimeMillis());
				try {
					writeLock.lock();
					Thread.sleep(2000);
					System.out.println(Thread.currentThread().getName() + "執行緒正在寫檔案...");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					writeLock.unlock();
				}
				System.out.println(Thread.currentThread().getName() + "結束:" + System.currentTimeMillis());
			}
		}, "寫1").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName() + "開始:" + System.currentTimeMillis());
				try {
					readLock.lock();
					Thread.sleep(1000);
					System.out.println(Thread.currentThread().getName() + "執行緒正在讀檔案...");
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					readLock.unlock();
				}
				System.out.println(Thread.currentThread().getName() + "結束:" + System.currentTimeMillis());
			}
		}, "讀2").start();
		
	}
}

執行結果:

1開始:15396934004001開始:15396934004012開始:15396934004021執行緒正在讀檔案...1結束:15396934014011執行緒正在寫檔案...1結束:15396934034012執行緒正在讀檔案...2結束:1539693404401

可以看出讀1執行緒花費1秒,寫1執行緒花費2秒,讀2執行緒花費1秒,共4秒; 如果我把執行緒啟動順序改為:讀1-讀2-寫1;執行結果為:

1開始:15396937334102開始:15396937334111開始:15396937334111執行緒正在讀檔案...2執行緒正在讀檔案...1結束:15396937344112結束:15396937344111執行緒正在寫檔案...1結束:1539693736411

可知讀1和讀2執行緒共花費1秒,寫執行緒花費2秒,共花費3秒;說明在沒有開啟寫執行緒之前,兩個讀執行緒認為是可以並行的,即使他們同時獲得readLock鎖。

原始碼分析

1 讀寫鎖的構造方法
public ReentrantReadWriteLock() {
        this(false);   // this方法表明還有一個帶有引數的構造方法 
    }

注意:說明同一個類的構造方法可以相互呼叫。

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

不帶引數的構造方法預設呼叫帶參的構造方法,引數為false。預設生成NonfairSync型別的物件。早在前面講過抽象類Sync的兩個實現類FairSync和NonfairSync,主要實現兩個方法:

writerShouldBlock();
readerShouldBlock();

注意:這裡通過判斷執行緒是否按照公平的方式獲得鎖來分為公平類和非公平類。這裡所謂公平的方式是指:獲取鎖的執行緒是否需要按照先後請求鎖的順序獲取鎖。看一下他們之間的繼承關係: 繼承關係 本例項中生成的是非公平類的物件nonFairSync,同時生成ReadLock物件、WriteLock物件。 讀寫鎖 在這裡,Lea大佬把讀寫鎖分開實現,然後又通過ReentrantReadWriteLock把兩者繫結在一起。

2 ReadLock類

類的所有方法:

public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
         
        /* 
* 如果寫鎖沒有被其他執行緒獲取,那麼立即返回讀鎖
         * 如果寫鎖被其執行緒他佔有,那麼請求讀鎖的執行緒進入休眠狀態直到獲得讀鎖
         */
        public void lock() {
            sync.acquireShared(1);
        }
        // 可中斷休眠狀態的獲取鎖的方法,其他與lock()相同
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
        /* 只有當寫鎖沒有被佔有的時候才會獲得讀鎖並返回true;即使採用的是公平策略;
         * 如果寫鎖被其他執行緒佔有則立即返回false;
         */ 
        public boolean tryLock() {
            return sync.tryReadLock();
        }
        // 等待讀鎖的執行緒等待timeOut時間,遵循公平策略;
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }
        // 釋放鎖,如果當前讀鎖執行緒數量為0,那麼可以開始寫鎖的獲取
        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 + "]";
        }

通過上面原始碼可知:寫鎖空閒是讀鎖獲取的前提(這點正好解釋例項中讀寫鎖獲取順序不同而執行時間不同);讀鎖的所有操作都是通過sync物件實現(sync物件分fairSync和NonfairSync)。

2.1 readLock.lock()
/* 在共享模式下請求讀鎖,並忽略中斷。
  *  tryAcquireShard(arg)至少執行一次,根據返回值判斷是否執行doAcquireShared(arg)
 */
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

當tryAcquireShared(arg)返回值為-1(小於0)則需要阻塞當前執行緒,表明無法獲得讀鎖;當返回值為1(大於0)時可以獲得讀鎖。

/*
* 該方法用於嘗試獲取讀鎖:
* 若寫鎖被佔有,則獲取讀鎖失敗;否則表明該執行緒可以獲得讀鎖;
* 若是公平策略則判斷是否等待,若無需等待則通過CAS操作更新state和count;
* 若獲取讀鎖失敗則採用迴圈重試機制;
*/
protected final int tryAcquireShared(int unused) {
            
            Thread current = Thread.currentThread();
            int c = getState();
            /*
             * exclusiveCount(c)結果表示state中獨佔持有數,即寫鎖持有數
			 * 若獨佔持有數不為零且當前執行緒不是持有寫鎖的執行緒,則返回-1
			 */ 
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // r表示讀鎖持有的執行緒數量
            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 != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

tryAcquireShard(arg)返回-1的情況: 寫鎖被其他執行緒佔有,返回-1,無法獲得讀鎖; tryAcquireShard(arg)返回1的情況: -readerShouldBlock()返回true且讀鎖計數器在合理範圍內,CAS更新state後,返回1,獲得讀鎖; 注意:readerShouldBlock()根據NonfairSync和FairSync類不同,策略也不一樣。FairSync判斷該執行緒前面是否有等待佇列,有則返回true,否則返回false;NonfairSync中,如果存在第一佇列執行緒正在排他狀態等待,則返回true. 如果這個方法返回true,並且當前執行緒正試圖以共享模式獲取,那麼可以保證當前執行緒不是第一個排隊的執行緒。

/*
* 進入該方法的前提是tryAcquireShard返回-1,即獲取讀鎖需要等待
* 當前獲取讀鎖的執行緒包裝為一個共享模式的node,並阻塞當前執行緒
*/
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

通過讀鎖的lock()方法可知: -當寫鎖的執行緒被佔有時,讀鎖獲取失敗; -當執行緒因為CAS操作或者其他執行緒阻塞等原因導致讀鎖獲取失敗則會不斷重試直到獲取讀鎖;

2.2 readLock.unLock()
// 若讀執行緒數為0,那麼鎖就可以用於寫鎖操作
public void unlock() {
            sync.releaseShared(1);
        }
/*
* 釋放一個共享的讀鎖
* 如果tryReleaseShared()返回true則執行doReleaseShared()釋放當前前程獲得的讀鎖
* 如果tryReleaseShared()返回fasle,則不釋放
*/
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

具體看一下如何釋放的:

// 返回true表明當前執行緒可以釋放;返回false則不釋放讀鎖
protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            // 當前執行緒是否是第一個讀鎖的執行緒
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            }
            //  否則holdCounter值減1
else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            // 自旋迴圈
            for (;;) {
                // 獲取當前執行緒的狀態值
                
            
           

相關推薦

實戰java併發程式設計ReentrantReadWriteLoc原始碼分析

前面分析了併發工具類CountDownLatch和CyclicBarrier,本文分享分析比較重要的ReentrantReadWriteLock。 使用場景 以前的同步方式需要對讀、寫操作進行同步,讀讀之間,讀寫之間,寫寫之間等;工程師們發現讀讀之間並不會影響資

實戰java併發程式設計CountDownLatch原始碼分析

首先看第一個! CountDownLatch 使用場景 CountDownLatch類是常見的併發同步控制類,適用於某一執行緒的執行在其他多個執行緒執行完成之後,比如火箭發射前需要各項指標檢查,只有當各項指標檢查完才能發射,再比如解析多個excel文件,只有當

實戰Java併發程式設計LockSupport

LockSupport簡介: LockSupport是一個非常方便實用的執行緒阻塞工具,它可以線上程任意位置讓執行緒阻塞.和Thread.suspend()相比,它彌補了由於resume()在前發生,導致執行緒無法繼續執行的情況.和Object.wait()相比,它不需要先

實戰Java併發程式設計Java記憶體模型和執行緒安全

Java記憶體模型 原子性: 是指一個操作是不可中斷的.即使多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾. 一般CPU的指令是原子的. Q:i++是原子操作嗎? A:不是.

實戰java併發程式設計 原始碼 source code

@rover這個是C++模板 --胡滿超 stack<Postion> path__;這個裡面 ”<> “符號是什麼意思?我在C++語言裡面沒見過呢? 初學者,大神勿噴。

實戰Java併發程式設計.epub

    【下載地址】 在過去單核CPU時代,單任務在一個時間點只能執行單一程式,隨著多核CPU的發展,並行程式開發就顯得尤為重要。 《實戰Java高併發程式設計》主要介紹基於Java的並行程式設計基礎、思路、方法和實戰。第一,立足於併發程式基礎,詳細介紹Ja

2018最新實戰Java併發程式設計

在過去單核CPU時代,單任務在一個時間點只能執行單一程式,隨著多核CPU的發展,並行程式開發就顯得尤為重要。《實戰Java高併發程式設計》主要介紹基於Java的並行程式設計基礎、思路、方法和實戰。第一,立足於併發程式基礎,詳細介紹Java中進行並行程式設計的基本方法。第二,進一步詳細介紹JDK中對並

Java併發程式設計synchronized關鍵字(二)

上一篇文章講了synchronized的部分關鍵要點,詳見:Java高併發程式設計之synchronized關鍵字(一) 本篇文章接著講synchronized的其他關鍵點。 在使用synchronized關鍵字的時候,不要以字串常量作為鎖定物件。看下面的例子: public class

Java併發程式設計synchronized關鍵字(一)

首先看一段簡單的程式碼: public class T001 { private int count = 0; private Object o = new Object(); public void m() { //任何執行緒要執行下面這段程式碼

實戰Java併發程式設計(五、並行模式與演算法)

5.1單例模式 單例模式:是一種常用的軟體設計模式,在它的核心結構中值包含一個被稱為單例的特殊類。一個類只有一個例項,即一個類只有一個物件例項。  對於系統中的某些類來說,只有一個例項很重要,例如,一個系統中可以存在多個列印任務,但是隻能有一個正在工作的任務;售票時,一共有100張票,可有有

實戰Java併發程式設計(四、鎖的優化及注意事項)

在多核時代,使用多執行緒可以明顯地提升系統的效能。但事實上,使用多執行緒會額外增加系統的開銷。對於單任務或單執行緒的應用來說,其主要資源消耗在任務本身。對於多執行緒來說,系統除了處理功能需求外,還需要維護多執行緒環境特有的資訊,如執行緒本身的元資料,執行緒的排程,執行緒上下文的切換等。 4.1有

實戰Java併發程式設計(3.2 執行緒池)

1.Executor jdk提供了一套Executor框架,本質上是一個執行緒池。 newFixedThreadPool()方法:該方法返回一個固定數量的執行緒池。該執行緒池中的執行緒數量始終不變,當有一個新任務提交時,執行緒池中若有空閒執行緒,則立即執行,若沒有,則任務會暫存在一個任

實戰Java併發程式設計(3.1同步控制)

3.1重入鎖 重入鎖使用java.util.concurrent.locks.ReentrantLock來實現 public class Test implements Runnable { public static ReentrantLock lock = new Reentr

實戰Java併發程式設計》學習總結(3)

第6章  java8與併發 1 顯式函式指函式與外界交換資料的唯一渠道就是引數和返回值,顯式函式不會去讀取或者修改函式的外部狀態。這樣的函式對於除錯和排錯是有益的。 2 函數語言程式設計式申明式的程式設計方式。而命令式則喜歡大量使用可變物件和指令。如下 // 指令式程式設計 p

實戰Java併發程式設計》學習總結(2)

第3章  JDK併發包 1 synchronized的功能擴充套件:重入鎖。使用java.util.concurrent.locks.ReentrantLock類來實現。 import java.util.concurrent.locks.ReentrantLock; publi

實戰Java併發程式設計》學習總結(1)

第1章 走入並行世界 1 併發(Concurrency)和並行(Parallelism)都可以表示兩個或多個任務一起執行。但併發偏重於多個任務交替執行,而多個任務之間有可能還是序列。並行是真正意義上的“同時執行”。 2 有關並行的兩個重要定律。Amdahl定律強調當序列比例一定時,加速比是有

頂級架構師學習——第二階段:實戰Java併發程式設計

1、什麼是並行? 並行處理(ParallelProcessing)是計算機系統中能同時執行兩個或更多個處理機的一種計算方法。處理機可同時工作於同一程式的不同方面。並行處理的主要目的是節省大型和複雜問題的解決時間。 2、為什麼需要並行? 平行計算只有在  影象處理  和 

Java併發程式設計第一階段,多執行緒基礎深入淺出

汪文君高併發程式設計第一階段01講-課程大綱及主要內容介紹 汪文君高併發程式設計第一階段02講-簡單介紹什麼是執行緒 汪文君高併發程式設計第一階段03講-建立並啟動執行緒 汪文君高併發程式設計第一階段04講-執行緒生命週期以及start方法原始碼剖析 汪文君高併發程式設計第

實戰Java併發程式設計》讀後感

寫在前面無關的內容        白駒過隙,看下日曆已經畢業4年多,加上在大學裡的4年,算算在計算機界也躺了八年,按照格拉德韋爾的1萬小時定律差不多我也該成為行業的專家了,然後並沒有。當看著“什麼是Java?”、“什麼是程式?”、“多執行緒是什麼?”、“怎麼構建一個合理的大型

實戰Java併發程式設計(一)走進併發世界

阻塞(blocking)一個執行緒是阻塞的,那麼其它的執行緒釋放資源之前,當前執行緒無法繼續執行。使用synchronized或者重入鎖會使執行緒這是。 無飢餓(starvation-free):對於非公平的鎖來說,系統允許高優先順序的執行緒插隊,會造成飢餓;而公平的鎖則不會造成飢餓。 無障礙(obstruc