高併發編程必備基礎


一、前言

借用Java併發編程實踐中的話"編寫正確的程序並不容易,而編寫正常的併發程序就更難了",相比於順序執行的情況,多線程的線程安全問題是微妙而且出乎意料的,因爲在沒有進行適當同步的情況下多線程中各個操作的順序是不可預期的,本文算是對多線程情況下同步策略的一個一個簡單介紹。

二、什麼是線程安全問題

線程安全問題是指當多個線程同時讀寫一個狀態變量,並且沒有任何同步措施時候,導致髒數據或者其他不可預見的結果的問題。Java中首要的同步策略是使用Synchronized關鍵字,它提供了可重入的獨佔鎖。

三、什麼是共享變量可見性問題

要談可見性首先需要介紹下多線程處理共享變量時候的Java中內存模型。

Java內存模型規定了所有的變量都存放在主內存中,當線程使用變量時候都是把主內存裏面的變量拷貝到了自己的工作空間或者叫做工作內存。

如圖是雙核CPU系統架構,每核有自己的控制器和運算器,其中控制器包含一組寄存器和操作控制器,運算器執行算術邏輯運算,並且有自己的一級緩存,並且有些架構裏面雙核還有個共享的二級緩存。
對應Java內存模型裏面的工作內存,在實現上這裏是指L1或者L2緩存或者自己cpu的寄存器

當線程操作一個共享變量時候操作流程爲:

  • 線程首先從主內存拷貝共享變量到自己的工作內存
  • 然後對工作內存裏的變量進行處理
  • 處理完後更新變量值到主內存

那麼假如線程AB同時去處理一個共享變量,會出現什麼情況那?
首先他們都會去走上面的三個流程,假如線程A拷貝共享變量到了工作內存,並且已經對數據進行了更新但是還沒有更新會主內存,這時候線程B拷貝共享變量到了自己的工內存進行處理,處理後,線程A才把自己的處理結果更更新到主內存,可知線程B處理的並不是線程A處理後的結果,也就是說線程A處理後的變量值對線程B不可見,這就是共享變量的可見性問題。

構成共享變量內存不可見原因是因爲三步流程不是原子性操作,下面知道使用恰當同步就可以解決這個問題。

我們知道ArrayList是線程不安全的,因爲他的讀寫方法沒有同步策略,會導致髒數據和不可預期的結果,下面我們就一一講解如何解決。

這是線程不安全的

publicclass ArrayList<E>

{

 

   public E get(intindex) {

       rangeCheck(index);

 

       return elementData(index);

   }

 

   public E set(intindex, E element) {

       rangeCheck(index);

 

       E oldValue = elementData(index);

       elementData[index]= element;

       return oldValue;

   }

}

四、原子性

4.1 介紹

假設線程A執行操作Ao和線程B執行操作Bo ,那麼從A看,當B線程執行Bo操作時候,那麼Bo操作全部執行,要麼全部不執行,我們稱AoBo操作互爲原子性操作,在設計計數器時候一般都是先讀取當前值,然後+1,然後更新會變量,是讀--寫的過程,這個過程必須是原子性的操作。

   publicclassThreadNotSafeCount {

 

       private  Long value;

 

       public Long getCount() {

            returnvalue;

       }

 

       publicvoidinc() {

            ++value;

       }

   }

如上代碼是線程不安全的,因爲不能保證++value是原子性操作。方法一是使用Synchronized進行同步如下:

   publicclassThreadSafeCount {

 

       private  Long value;

 

       public synchronized Long getCount() {

           returnvalue;

       }

 

       public synchronized voidinc() {

            ++value;

       }

   }

注意,這裏不能簡單的使用volatile修飾value進行同步,因爲變量值依賴了當前值

使用Synchronized確實可以實現線程安全,即實現可見性和同步,但是Synchronized是獨佔鎖,沒有獲取內部鎖的線程會被阻塞掉,那麼有沒有剛好的實現那?答案是肯定的。

4.2 原子變量類

原子變量類比鎖更輕巧,比如AtomicLong代表了一個Long值,並提供了get,set方法,getset方法語音和volatile相同,因爲AtomicLong內部就是使用了volatile修飾的真正的Long變量。另外提供了原子性的自增自減操作,所以計數器可以改下爲:

 

   publicclassThreadSafeCount {

 

       private  AtomicLong value = new AtomicLong(0L);

 

       public  Long getCount() {

            returnvalue.get();

       }

 

       publicvoidinc() {

            value.incrementAndGet();

       }

   }

那麼相比使用synchronized的好處在於原子類操作不會導致線程的掛起和重新調度,因爲他內部使用的是cas的非阻塞算法。

常用的原子類變量爲:AtomicLongAtomicIntegerAtomicBoolean,AtomicReference.

CAS介紹

CAS CompareAndSet,也就是比較並設置,CAS有三個操作數分別爲:內存位置,舊的預期值,新的值,操作含義是當內存位置的變量值爲舊的預期值時候使用新的值替換舊的值。通俗的說就是看內存位置的變量值是不是我給的舊的預期值,如果是則使用我給的新的值替換他,如果不是返回給我舊值。這個是處理器提供的一個原子性指令。上面介紹的AtomicLong的自增就是使用這種方式實現:

   publicfinallongincrementAndGet(){

       for (;;) {

           long current = get();1

            long next = current + 1;2

            if (compareAndSet(current, next))3

                return next;

       }

   }

 

   publicfinalbooleancompareAndSet(long expect, long update) {

       return unsafe.compareAndSwapLong(this, valueOffset, expect, update);

   }

 

假如當前值爲1,那麼線程A和檢查B同時執行到了(3)時候各自的next都是2current=1,假如線程A先執行了3,那麼這個是原子性操作,會把檔期值更新爲2並且返回1if判斷true所以incrementAndGet返回2.這時候線程B執行3,因爲current=1而當前變量實際值爲2,所以if判斷爲false,繼續循環,如果沒有其他線程去自增變量的話,這次線程B就會更新變量爲3然後退出。

這裏使用了無限循環使用CAS進行輪詢檢查,雖然一定程度浪費了cpu資源,但是相比鎖來說避免的線程上下文切換和調度。

六、什麼是可重入鎖

當一個線程要獲取一個被其他線程佔用的鎖時候,該線程會被阻塞,那麼當一個線程再次獲取它自己已經獲取的鎖時候是否會被阻塞那?如果不需要阻塞那麼我們說該鎖是可重入鎖,也就是鎖只要該線程獲取了該鎖,那麼可以無限制次數進入被該鎖鎖住的代碼。

先看一個例子如果鎖不是可重入的,看看會出現什麼問題。

publicclassHello{

    public Synchronized voidhelloA(){

       System.out.println("hello");

    }

 

    public Synchronized voidhelloB(){

       System.out.println("hello B");

       helloA();

    }

 

}

如上面代碼當調用helloB函數前會先獲取內置鎖,然後打印輸出,然後調用helloA方法,調用前會先去獲取內置鎖,如果內置鎖不是可重入的那麼該調用就會導致死鎖了,因爲線程持有並等待了鎖導致helloB永遠不會獲取內置鎖。

實際上內部鎖是可重入鎖,例如synchronized關鍵字管理的方法,可重入鎖的原理是在所內部維護了一個線程標示標示該鎖目前被那個線程佔用,然後關聯一個計數器,一開始計數器值爲0,說明該鎖沒有被任何線程佔用,當一個線程獲取了該鎖,計數器會變成1,其他線程在獲取該鎖時候發現鎖的所有者不是自己所以被阻塞,但是當獲取該鎖的線程再次獲取鎖時候發現鎖擁有者是自己會把計數器值+1當釋放鎖後計數器會-1,當計數器爲0時候,鎖裏面的線程標示重置爲null,這時候阻塞的線程會獲取被喚醒來獲取該鎖。

七、Synchronized關鍵字

7.1 Synchronized介紹

synchronized塊是Java提供的一種強制性內置鎖,每個Java對象都可以隱式的充當一個用於同步的鎖的功能,這些內置的鎖被稱爲內部鎖或者叫監視器鎖,執行代碼在進入synchronized代碼塊前會自動獲取內部鎖,這時候其他線程訪問該同步代碼塊時候會阻塞掉。拿到內部鎖的線程會在正常退出同步代碼塊或者異常拋出後釋放內部鎖,這時候阻塞掉的線程才能獲取內部鎖進入同步代碼塊。

7.2 Synchronized同步實例

內部鎖是一種互斥鎖,具體說是同時已有一個線程可以拿到該鎖,當一個線程拿到該鎖並且沒有釋放的情況下,其他線程只能等待。

對於上面說的ArrayList可以使用synchronized進行同步來處理可見性問題。

使用synchronized對方法進行同步

publicclass ArrayList<E>

{

 

   public synchronized  E get(intindex) {

       rangeCheck(index);

 

        return elementData(index);

   }

 

   public synchronized E set(intindex, E element) {

       rangeCheck(index);

 

       E oldValue = elementData(index);

       elementData[index]= element;

       return oldValue;

   }

}

如圖當線程A獲取內部鎖進入同步代碼塊後,線程B也準備要進入同步塊,但是由於A還沒釋放鎖,所以B現在進入等待,使用同步可以保證線程A獲取鎖到釋放鎖期間的變量值對B獲取鎖後都可見。也就是說當B開始執行A執行的代碼同步塊時候可以看到A操作的所有變量值,這裏具體說是當線程B獲取b的值時候能夠保證獲取的值是2。這時因爲線程A進入同步塊修改變量值後,會在退出同步塊前把值刷新到主內存,而線程B在進入同步塊前會首先清空本地內存內容,從主內存重新獲取變量值,所以實現了可見性。但是要注意一點所有線程使用的是同一個鎖。

注意 Synchronized關鍵字會引起現場上下文切換和線程調度

八、ReentrantReadWriteLock介紹

使用synchronized可以實現同步,但是缺點是同時只有一個線程可以訪問共享變量,但是正常情況下,對於多個讀操作操作共享變量時候是不需要同步的,synchronized時候無法實現多個讀線程同時執行,而大部分情況下讀操作次數多於寫操作,所以這大大降低了併發性,所以出現了ReentrantReadWriteLock,它可以實現讀寫分離,運行多個線程同時進行讀取,但是最多運行一個寫現線程存在。

對於上面的方法現在可以修改爲:

 

publicclass ArrayList<E>

{

 privatefinal ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

 

 public E get(intindex) {

 

       Lock readLock = readWriteLock.readLock();

       readLock.lock();

       try {

            return list.get(index);

       } finally {

            readLock.unlock();

       }

   }

 

   public E set(intindex, E element) {

 

       Lock wirteLock = readWriteLock.writeLock();

       wirteLock.lock();

       try {

            return list.set(index,element);

       } finally {

            wirteLock.unlock();

       }

   }

}

如代碼在get方法時候通過 readWriteLock.readLock()獲取了讀鎖,多個線程可以同時獲取這讀鎖,set方法通過readWriteLock.writeLock()獲取了寫鎖,同時只有一個線程可以獲取寫鎖,其他線程在獲取寫鎖時候會阻塞直到寫鎖被釋放。假如一個線程已經獲取了讀鎖,這時候如果一個線程要獲取寫鎖時候要等待直到釋放了讀鎖,如果一個線程獲取了寫鎖,那麼所有獲取讀鎖的線程需要等待直到寫鎖被釋放。所以相比synchronized來說運行多個讀者同時存在,所以提高了併發量。
注意 需要使用者顯示調用Lockunlock操作

九、Volatile變量

對於避免不可見性問題,Java還提供了一種弱形式的同步,即使用了volatile關鍵字。該關鍵字確保了對一個變量的更新對其他線程可見。當一個變量被聲明爲volatile時候,線程寫入時候不會把值緩存在寄存器或者或者在其他地方,當線程讀取的時候會從主內存重新獲取最新值,而不是使用當前線程的拷貝內存變量值。

volatile雖然提供了可見性保證,但是不能使用他來構建複合的原子性操作,也就是說當一個變量依賴其他變量或者更新變量值時候新值依賴當前老值時候不在適用。與synchronized相似之處在於如圖

如圖線程A修改了volatile變量b的值,然後線程B讀取了改變量值,那麼所有A線程在寫入變量b值前可見的變量值,在B讀取volatile變量b後對線程B都是可見的,途中線程BA操作的變量a,b的值都可見的。volatile的內存語義和synchronized有類似之處,具體說是說當線程寫入了volatile變量值就等價於線程退出synchronized同步塊(會把寫入到本地內存的變量值同步到主內存),讀取volatile變量值就相當於進入同步塊(會先清空本地內存變量值,從主內存獲取最新值)。

下面的Integer也是線程不安全的,因爲沒有進行同步措施

   publicclassThreadNotSafeInteger {

 

       privateintvalue;

 

       publicintget() {

            returnvalue;

       }

 

       publicvoidset(intvalue) {

            this.value = value;

       }

   }

 

使用synchronized關鍵字進行同步如下:

   publicclassThreadSafeInteger {

 

       privateintvalue;

 

       public synchronized intget() {

            returnvalue;

       }

 

       public synchronized  voidset(intvalue) {

            this.value = value;

       }

   }

等價於使用volatile進行同步如下:

   publicclassThreadSafeInteger {

 

       privatevolatileintvalue;

 

       publicintget() {

            returnvalue;

       }

 

       publicvoidset(intvalue) {

            this.value = value;

       }

   }

這裏使用synchronized和使用volatile是等價的,但是並不是所有情況下都是等價,一般只有滿足下面所有條件才能使用volatile

  • 寫入變量值時候不依賴變量的當前值,或者能夠保證只有一個線程修改變量值。
  • 寫入的變量值不依賴其他變量的參與。
  • 讀取變量值時候不能因爲其他原因進行枷鎖。

另外加鎖可以同時保證可見性和原子性,而volatile只保證變量值的可見性。

注意 volatile關鍵字不會引起線程上下文切換和線程調度

十、樂觀鎖與悲觀鎖

10.1 悲觀鎖

悲觀鎖,指數據被外界修改持保守態度(悲觀),在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制。數據庫中實現是對數據記錄進行操作前,先給記錄加排它鎖,如果獲取鎖失敗,則說明數據正在被其他線程修改,則等待或者拋出異常。如果加鎖成功,則獲取記錄,對其修改,然後事務提交後釋放排它鎖。
一個例子:select * from where .. for update;

悲觀鎖是先加鎖再訪問策略,處理加鎖會讓數據庫產生額外的開銷,還有增加產生死鎖的機會,另外在多個線程只讀情況下不會產生數據不一致行問題,沒必要使用鎖,只會增加系統負載,降低併發性,因爲當一個事務鎖定了該條記錄,其他讀該記錄的事務只能等待。

10.2 樂觀鎖

樂觀鎖是相對悲觀鎖來說的,它認爲數據一般情況下不會造成衝突,所以在訪問記錄前不會加排他鎖,而是在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,具體說根據update返回的行數讓用戶決定如何去做。樂觀鎖並不會使用數據庫提供的鎖機制,一般在表添加version字段或者使用業務狀態來做。
具體可以參考:https://www.atatech.org/articles/79240

樂觀鎖直到提交的時候纔去鎖定,所以不會產生任何鎖和死鎖。

十一、獨佔鎖與共享鎖

根據鎖能夠被單個線程還是多個線程共同持有,鎖又分爲獨佔鎖和共享鎖。獨佔鎖保證任何時候都只有一個線程能讀寫權限,ReentrantLock就是以獨佔方式實現的互斥鎖。共享鎖則可以同時有多個讀線程,但最多只能有一個寫線程,讀和寫是互斥的,例如ReadWriteLock讀寫鎖,它允許一個資源可以被多線程同時進行讀操作,或者被一個線程寫操作,但兩者不能同時進行。

獨佔鎖是一種悲觀鎖,每次訪問資源都先加上互斥鎖,這限制了併發性,因爲讀操作並不會影響數據一致性,而獨佔鎖只允許同時一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取。

共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。

十二、公平鎖與非公平鎖

根據線程獲取鎖的搶佔機制鎖可以分爲公平鎖和非公平鎖,公平鎖表示線程獲取鎖的順序是按照線程加鎖的時間多少來分決定的的,也就是最早枷鎖的線程將最早獲取鎖,也就是先來先得的FIFO順序。而非公平鎖則運行闖入,也就是先來不一定先得。

ReentrantLock提供了公平和非公平鎖的實現:
公平鎖ReentrantLock pairLock = newReentrantLock(true);
非公平鎖 ReentrantLock pairLock = newReentrantLock(false);
如果構造函數不傳遞參數,則默認是非公平鎖。

在沒有公平性需求的前提下儘量使用非公平鎖,因爲公平鎖會帶來性能開銷。
假設線程A已經持有了鎖,這時候線程B請求該鎖將會被掛起,當線程A釋放鎖後,假如當前有線程C也需要獲取該鎖,如果採用非公平鎖方式,則根據線程調度策略線程BC兩者之一可能獲取鎖,這時候不需要任何其他干涉,如果使用公平鎖則需要把C掛起,讓B獲取當前鎖。

十三、AbstractQueuedSynchronizer介紹

AbstractQueuedSynchronizer提供了一個隊列,大多數開發者可能從來不會直接用到AQSAQS中刮泥這個一個單一的狀態信息 state,可以通過protectedgetState,setState,compareAndSetState函數進行調用。對於ReentrantLock來說,state可以用來表示該線程獲可重入鎖的次數,semaphore來說state用來表示當前可用信號的個數,FutuerTask用來表示任務狀態(例如還沒開始,運行,完成,取消)。

十四、CountDownLatch原理

14.1 一個例子

publicclassTest {

 

   privatestaticfinalint ThreadNum = 10;

 

   publicstaticvoid main(String[] args)  {

 

       //創建一個CountDownLatch實例,管理計數爲ThreadNum

       CountDownLatch countDownLatch = new CountDownLatch(ThreadNum);

 

       //創建一個固定大小的線程池

       ExecutorService executor = Executors.newFixedThreadPool(ThreadNum);

 

       //添加線程到線程池

       for(int i =0;i<ThreadNum;++i){

            executor.execute(new Person(countDownLatch, i+1));

       }

 

       System.out.println("開始等待全員簽到...");

 

       try {

            //等待所有線程執行完畢

            countDownLatch.await();

            System.out.println("簽到完畢,開始吃飯");

 

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }finally {

            executor.shutdown();

       }

 

   }

 

   staticclass Person implementsRunnable{

 

       private CountDownLatchcountDownLatch;

       privateintindex;

 

       public Person(CountDownLatchcdl,intindex){

            this.countDownLatch = cdl;

            this.index = index;

       }

 

       @Override

       publicvoid run() {

 

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                //TODO Auto-generated catch block

                e.printStackTrace();

            }

            System.out.println("person " + index +"簽到");

 

            //線程執行完畢,計數器減一

            countDownLatch.countDown();

 

        }

 

   }

}

如上代碼,創建一個線程池和CountDownLatch實例,每個線程通過構造函數傳入CountDownLatch的實例,主線程通過await等待線程池裏面線程任務全部執行完畢,子線程則執行完畢後調用countDown計數器減一,等所有子線程執行完畢後,主線程的await纔會返回。

14.2 原理

先看下類圖:

可知CountDownLatch內部還是使用AQS實現的。
首先通過構造函數初始化AQS的狀態值

 

   public CountDownLatch(intcount) {

       if (count < 0) thrownew IllegalArgumentException("count < 0");

       this.sync = new Sync(count);

   }

       Sync(intcount) {

            setState(count);

       }

然後看下await方法:

   publicfinalvoidacquireSharedInterruptibly(int arg)

            throws InterruptedException {

       //如果線程被中斷則拋異常

       if (Thread.interrupted())

            thrownew InterruptedException();

       //嘗試看當前是否計數值爲0,爲0則直接返回,否者進入隊列等待

       if (tryAcquireShared(arg)< 0)

            doAcquireSharedInterruptibly(arg);

   }

 

 protectedinttryAcquireShared(int acquires) {

            return (getState() == 0) ? 1 : -1;

       }

如果tryAcquireShared返回-1進入doAcquireSharedInterruptibly

   private void doAcquireSharedInterruptibly(int arg)

       throws InterruptedException {

      //加入隊列狀態爲共享節點

       final Nodenode = addWaiter(Node.SHARED);

       boolean failed = true;

       try {

            for (;;) {

                final Nodep = node.predecessor();

                if (p == head) {

                    int r =tryAcquireShared(arg);

                    if (r >= 0) {

                       //如果多個線程調用了await被放入隊列則一個個返回。

                        setHeadAndPropagate(node, r);

                        p.next = null; // helpGC

                        failed = false;

                        return;

                    }

                }

                //shouldParkAfterFailedAcquire會把當前節點狀態變爲SIGNAL類型,然後調用park方法把當先線程掛起,

                if(shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    throw newInterruptedException();

            }

       } finally {

            if (failed)

                cancelAcquire(node);

       }

   }

調用await後,當前線程會被阻塞主,知道所有子線程調用了countdown方法,並在在計數爲0時候調用該線程unpark方法激活線程,然後該線程重新tryAcquireShared會返回1

然後看下 countDown方法:

委託給sync

   publicvoidcountDown(){

       sync.releaseShared(1);

   }

   publicfinalbooleanreleaseShared(int arg) {

       if (tryReleaseShared(arg)){

            doReleaseShared();

            returntrue;

       }

       returnfalse;

   }

首先看下tryReleaseShared

       protectedbooleantryReleaseShared(int releases) {

            //循環進行cas,直到當前線程成功完成cas使計數值(狀態值state)減一更新到state

            for (;;) {

                int c = getState();

                if (c == 0)

                    returnfalse;

                int nextc = c-1;

                if (compareAndSetState(c, nextc))

                    return nextc == 0;

            }

       }

 

該函數一直返回false直到當前計數器爲0時候才返回true
返回true後會調用doReleaseShared,該函數主要作用是調用uppark方法激活調用await的線程,代碼如下:

privatevoiddoReleaseShared() {

 

   for (;;) {

       Node h = head;

       if (h != null && h != tail) {

            int ws = h.waitStatus;

            //節點類型爲SIGNAL,把類型在通過cas設置回去,然後調用unpark激活調用await的線程

            if (ws == Node.SIGNAL) {

                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

                    continue;            // loop to recheck cases

                unparkSuccessor(h);

            }

            elseif (ws == 0 &&

                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

                continue;                // loop on failed CAS

       }

       if (h == head)                   //loop if head changed

            break;

   }

}

激活主線程後,主線程會在調用tryAcquireShared獲取鎖。

十五、ReentrantLock獨佔鎖原理

15.1 ReentrantLock結構

先上類圖:

可知ReentrantLock最終還是使用AQS來實現,並且根據參數決定內部是公平還是非公平鎖,默認是非公平鎖

 publicReentrantLock(){

       sync = new NonfairSync();

   }

 

   publicReentrantLock(boolean fair) {

       sync = fair ? new FairSync() : newNonfairSync();

   }

加鎖代碼:

 

publicvoidlock() {

       sync.lock();

   }

 

 

15.2 公平鎖原理

先看Lock方法:
lock
方法最終調用FairSync重寫的tryAcquire方法

       protectedfinalbooleantryAcquire(int acquires) {

            //獲取當前線程和狀態值

            final Thread current = Thread.currentThread();

            int c = getState();

           //狀態爲0說明該鎖未被任何線程持有

            if (c == 0) {

             //爲了實現公平,首先看隊列裏面是否有節點,有的話再看節點所屬線程是不是當前線程,是的話hasQueuedPredecessors返回false,然後使用原子操作compareAndSetState保證一個線程更新狀態爲1,設置排他鎖歸屬與當前線程。其他線程通過cass則返回false.

                if (!hasQueuedPredecessors() &&

                    compareAndSetState(0, acquires)) {

                   setExclusiveOwnerThread(current);

                    returntrue;

                }

            }

//狀態不爲0說明該鎖已經被線程持有,則看是否是當前線程持有,是則重入鎖次數+1.

            elseif (current ==getExclusiveOwnerThread()) {

                int nextc = c + acquires;

                if (nextc < 0)

                    thrownew Error("Maximum lock count exceeded");

 

                setState(nextc);

                returntrue;

            }

            returnfalse;

       }

   }

公平性保證代碼:

 

   public final boolean hasQueuedPredecessors() {

 

       Nodet = tail; // Read fields in reverse initialization order

       Node h = head;

       Nodes;

       return h != t &&

            ((s = h.next) == null || s.thread!= Thread.currentThread());

   }

再看看unLock方法,最終調用了SynctryRelease方法:

       protectedfinalbooleantryRelease(int releases) {

           //如果不是鎖持有者調用UNlock則拋出異常。

            int c = getState() - releases;

            if (Thread.currentThread() != getExclusiveOwnerThread())

                thrownewIllegalMonitorStateException();

            boolean free = false;

           //如果當前可重入次數爲0,則清空鎖持有線程

            if (c == 0) {

                free = true;

                setExclusiveOwnerThread(null);

            }

            //設置可重入次數爲原始值-1

            setState(c);

            return free;

       }

15.3 非公平鎖原理

       finalvoidlock() {

 

           //如果當前鎖空閒0,則設置狀態爲1,並且設置當前線程爲鎖持有者

            if (compareAndSetState(0, 1))

               setExclusiveOwnerThread(Thread.currentThread());

            else

                acquire(1);//調用重寫的tryAcquire方法-nonfairTryAcquire方法

       }

 finalbooleannonfairTryAcquire(int acquires) {

            final Thread current = Thread.currentThread();

            int c = getState();

            if (c == 0) {//狀態爲0說明沒有線程持有該鎖

                if (compareAndSetState(0, acquires)) {//cass原子性操作,保證只有一個線程可以設置狀態

                   setExclusiveOwnerThread(current);//設置鎖所有者

                    returntrue;

                }

            }//如果當前線程是鎖持有者則可重入鎖計數+1

            elseif (current ==getExclusiveOwnerThread()) {

                int nextc = c + acquires;

                if (nextc < 0)// overflow

                    thrownew Error("Maximum lock count exceeded");

                setState(nextc);

                returntrue;

            }

            returnfalse;

       }

15.3 總結

可知公平與非公平都是先執行tryAcquire嘗試獲取鎖,如果成功則直接獲取鎖,如果不成功則把當前線程放入隊列。對於放入隊列裏面的第一個線程Aunpark後會進行自旋調用tryAcquire嘗試獲取鎖,假如這時候有一個線程B執行了lock操作,那麼也會調用tryAcquire方法嘗試獲取鎖,但是線程B並不在隊列裏面,但是線程B有可能比線程A優先獲取到鎖,也就是說雖然線程A先請求的鎖,但是卻有可能沒有B先獲取鎖,這是非公平鎖實現。而公平鎖要保證線程A要比線程B先獲取鎖。所以公平鎖相比非公平鎖在tryAcquire裏面添加了hasQueuedPredecessors方法用來保證公平性。

十六、ReentrantReadWriteLock原理

如圖讀寫鎖內部維護了一個ReadLockWriteLock,並且也提供了公平和非公平的實現,下面只介紹下非公平的讀寫鎖實現。我們知道AQS裏面只維護了一個state狀態,而ReentrantReadWriteLock則需要維護讀狀態和寫狀態,一個state是無法表示寫和讀狀態的。所以ReentrantReadWriteLock使用state的高16位表示讀狀態也就是讀線程的個數,低16位表示寫鎖可重入量。

staticfinalint SHARED_SHIFT   = 16;

 

共享鎖(讀鎖)狀態單位值65536

staticfinalint SHARED_UNIT    = (1 << SHARED_SHIFT);

共享鎖線程最大個數65535

staticfinalint MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

 

排它鎖(寫鎖)掩碼二進制151

staticfinalint EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

 

/**返回讀鎖線程數  */

staticint sharedCount(int c)    { return c >>> SHARED_SHIFT; }

/**返回寫鎖可重入個數 */

staticint exclusiveCount(int c) { return c & EXCLUSIVE_MASK;}

 

16.1 WriteLock

  • lock 獲取鎖 對應寫鎖只需要分析下Sync的tryAcquire和tryRelease ```java

protected final boolean tryAcquire(int acquires) {

       Thread current = Thread.currentThread();

       int c = getState();

       int w = exclusiveCount(c);

       //c!=0說明讀鎖或者寫鎖已經被某線程獲取

       if (c != 0) {

            //w=0說明已經有線程獲取了讀鎖或者w!=0並且當前線程不是寫鎖擁有者,則返回false

            if (w == 0 || current !=getExclusiveOwnerThread())

                returnfalse;

           //說明某線程獲取了寫鎖,判斷可重入個數

            if (w + exclusiveCount(acquires) > MAX_COUNT)

                throw new Error("Maximum lock count exceeded");

 

           //設置可重入數量(1)

           setState(c + acquires);

            returntrue;

       }

 

      //第一個寫線程獲取寫鎖

       if (writerShouldBlock() ||

            !compareAndSetState(c, c + acquires))

            returnfalse;

       setExclusiveOwnerThread(current);

       returntrue;

   }

 

- unlock 釋放鎖

 

```Java

       protectedfinalbooleantryRelease(int releases) {

    // 看是否是寫鎖擁有者調用的unlock

            if (!isHeldExclusively())

                thrownewIllegalMonitorStateException();

//獲取可重入值,這裏沒有考慮高16位,因爲寫鎖時候讀鎖狀態值肯定爲0

           int nextc = getState() - releases;

            boolean free = exclusiveCount(nextc) == 0;

      //如果寫鎖可重入值爲0則釋放鎖,否者只是簡單更新狀態值。

            if (free)

                setExclusiveOwnerThread(null);

            setState(nextc);

            return free;

       }

16.2 ReadLock

對應讀鎖只需要分析下SynctryAcquireSharedtryReleaseShared

  • lock 獲取鎖

·        protected final int tryAcquireShared(int unused) {

·         

·        //獲取當前狀態值

·        Thread current = Thread.currentThread();

·        int c = getState();

·         

·        //如果寫鎖計數不爲0說明已經有線程獲取了寫鎖,然後看是不是當前線程獲取的寫鎖。

·        if (exclusiveCount(c) != 0 &&

·            getExclusiveOwnerThread() != current)

·            return -1;

·         

·        //獲取讀鎖計數

·        int r = sharedCount(c);

·        //嘗試獲取鎖,多個讀線程只有一個會成功,不成功的進入下面fullTryAcquireShared進行重試

·        if (!readerShouldBlock() &&

·            r < MAX_COUNT &&

·            compareAndSetState(c, c + SHARED_UNIT)) {

·            if (r == 0) {

·                firstReader = current;

·                firstReaderHoldCount = 1;

·            } elseif (firstReader ==current) {

·                firstReaderHoldCount++;

·            } else {

·                HoldCounter rh = cachedHoldCounter;

·                if (rh == null || rh.tid != current.getId())

·                    cachedHoldCounter = rh = readHolds.get();

·                elseif (rh.count == 0)

·                    readHolds.set(rh);

·                rh.count++;

·            }

·            return1;

·        }

·        return fullTryAcquireShared(current);

·        }

  • unlock 釋放鎖

·        protectedfinalboolean tryReleaseShared(int unused) {

·        Thread current =Thread.currentThread();

·        if (firstReader == current) {

·            // assertfirstReaderHoldCount > 0;

·            if (firstReaderHoldCount == 1)

·                firstReader = null;

·            else

·                firstReaderHoldCount--;

·        } else {

·            HoldCounter rh = cachedHoldCounter;

·            if (rh == null || rh.tid !=current.getId())

·                rh = readHolds.get();

·            intcount = rh.count;

·            if (count <= 1) {

·                readHolds.remove();

·                if (count <= 0)

·                    throw unmatchedUnlockException();

·            }

·            --rh.count;

·        }

·         

·        //循環直到自己的讀計數-1 cas更新成功

·        for (;;) {

·            int c = getState();

·            int nextc = c - SHARED_UNIT;

·            if (compareAndSetState(c, nextc))

·         

·                return nextc == 0;

·        }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章