Synchronized&Lock&AQS詳解


  加鎖目的:由於線程執行的過程是不可控的,所以需要采用同步機制來協同對對象可變狀態的訪問。

  加鎖方式:java鎖分為兩種--顯示鎖和隱示鎖,本質區別在於顯示鎖需要的是程序員自己手動的進行加鎖與解鎖如ReentrantLock需要進行lock與unlock。而隱式鎖則是Synchronized,jvm內置鎖,jvm進行操作加鎖與解鎖。

Synchronized關鍵字

  每個對象創建后都會存在一個Monitor(監視器鎖),它的實現依賴底層的系統的Mutex Lock(互斥鎖)實現,是重量級鎖,但是在java1.6版本之后,jvm內置鎖進行了一系列的優化,如:鎖粗化、鎖消除、偏向鎖、輕量級鎖、重量級鎖等。

  Synchronized鎖編譯成字節碼后,會發現jvm底層使用了monitorenter與monitorexit來進行加鎖與解鎖

  

我們知道synchronized加鎖加在對象上,對象是如何記錄鎖狀態的呢?

  答案是鎖狀態是被記錄在每個對象的對象頭(Mark Word)中,下面我們一起認識一下對象的內存布局

對象的內存布局

  HotSpot虛擬機中,對象在內存中存儲的布局可以分為三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

  • 對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態標志,偏向鎖(線程)ID,偏向時間,數組長度(數組對象)等
  • 實例數據:即創建對象時,對象中成員變量,方法等
  • 對齊填充:對象的大小必須是8字節的整數倍

AQS具備特性

  • 阻塞等待隊列
  • 公平/非公平
  • 可重入
  • 共享/獨占
  • 允許中斷

  例如Java.concurrent.util當中同步器的實現如Lock,Latch,Barrier等,都是基於AQS框架實現,一般通過定義內部類Sync繼承AQS,將同步器所有調用都映射到Sync對應的方法

 1 static final class NonfairSync extends Sync {
 2         private static final long serialVersionUID = 7316153563782823691L;
 3 
 4         /**
 5          * Performs lock.  Try immediate barge, backing up to normal
 6          * acquire on failure.
 7          */
 8         final void lock() {
 9             if (compareAndSetState(0, 1))
10                 setExclusiveOwnerThread(Thread.currentThread());
11             else
12                 acquire(1);
13         }
14 
15         protected final boolean tryAcquire(int acquires) {
16             return nonfairTryAcquire(acquires);
17         }
18     }

  該NonfairSync為ReentranLock內部類,默認非公平鎖,非公平鎖與公平鎖的區別在於,當正在爭搶的鎖釋放時,誰會搶到鎖。我們再看一下 公平鎖的lock()方法,就明白了。

 1 static final class FairSync extends Sync {
 2         private static final long serialVersionUID = -3000897897090466540L;
 3 
 4         final void lock() {
 5             acquire(1);
 6         }
 7 
 8         /**
 9          * Fair version of tryAcquire.  Don't grant access unless
10          * recursive call or no waiters or is first.
11          */
12         protected final boolean tryAcquire(int acquires) {
13             final Thread current = Thread.currentThread();
14             int c = getState();
15             if (c == 0) {
16                 if (!hasQueuedPredecessors() &&
17                     compareAndSetState(0, acquires)) {
18                     setExclusiveOwnerThread(current);
19                     return true;
20                 }
21             }
22             else if (current == getExclusiveOwnerThread()) {
23                 int nextc = c + acquires;
24                 if (nextc < 0)
25                     throw new Error("Maximum lock count exceeded");
26                 setState(nextc);
27                 return true;
28             }
29             return false;
30         }
31     }

  前面看到非公平鎖首先去嘗試設置state狀態是否可以成功,這里的state為Node節點的屬性,默認為0就是沒有人搶到鎖的情況,加一次鎖就會state進行CAS操作+1,因為它是可重入的鎖沒所以每加一次都會state+1,沒釋放一次都會-1,直到state為0時,才會輪到下一個線程進行搶鎖。我們再看一下acquire(1)方法,就明白了。

1 public final void acquire(int arg) {
2         if (!tryAcquire(arg) &&
3             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4             selfInterrupt();
5     }
我來講接一下:tryAcquire(arg)是嘗試獲取鎖,並將state狀態由0變為+1,如果失敗說明鎖已經被別人搶走了,需要下一步操作就是addWaiter
       addWaiter()方法就是創建一個雙向鏈表的結構的隊列,看到它是一種Node.EXCLUSIVE模式創建的,為獨占模式
       acquireQueued()方法讓第一個節點去爭搶鎖,如果失敗則會將該線程直接中斷阻塞,如果當前節點的前一個節點為頭結點也就是第一個節點搶到鎖了將會把當前節點作為新的頭結點並返回值
tryAcquire(arg)公平鎖與非公平鎖實現不一樣,公平鎖多判斷了一步就是如果我的同步隊列也就是等待隊列里有其他等待的節點,將不會讓當前的新線程去獲取鎖。

  釋放鎖的時候是一樣的邏輯
1 public final boolean release(int arg) {
2         if (tryRelease(arg)) {
3             Node h = head;
4             if (h != null && h.waitStatus != 0)
5                 unparkSuccessor(h);
6             return true;
7         }
8         return false;
9     }
  也是會改變state值的狀態,當變為0時,Node還有一個屬性就是exclusiveOwnerThread,它指向的是正在使用鎖的線程,state釋放為初始狀態的時候,exclusiveOwnerThread將會置位null;去喚醒頭結點。取消阻塞公平與非公平就在於去搶鎖的時候判斷是不一樣的。

BlockingQueue實現原理

  我們以ArrayBlockingQueue為例講解為什么AQS需要使用同步隊列與條件隊列兩個隊列;
 1  public ArrayBlockingQueue(int capacity) {
 2         this(capacity, false);
 3     }
 4  public ArrayBlockingQueue(int capacity, boolean fair) {
 5         if (capacity <= 0)
 6             throw new IllegalArgumentException();
 7         this.items = new Object[capacity];
 8         lock = new ReentrantLock(fair);
 9         notEmpty = lock.newCondition();
10         notFull =  lock.newCondition();
11     }

 

     源碼中我們創建阻塞隊列需要創建容量初始大小以及默認非公平鎖,底層看到使用的是獨占鎖ReentrantLock以及兩個條件隊列Condition;

 1 public void put(E e) throws InterruptedException {
 2         checkNotNull(e);
 3         final ReentrantLock lock = this.lock;
 4         lock.lockInterruptibly();
 5         try {
 6             while (count == items.length)
 7                 notFull.await();
 8             enqueue(e);
 9         } finally {
10             lock.unlock();
11         }
12     }

  當我們往隊列中添加元數時,則會將元素放入到數組中,並且喚醒阻塞線程;如果數組填滿的時候,則會將當前阻塞,我們看看await方法;

 1 public final void await() throws InterruptedException {
 2             if (Thread.interrupted())
 3                 throw new InterruptedException();
 4             Node node = addConditionWaiter();
 5             int savedState = fullyRelease(node);
 6             int interruptMode = 0;
 7             while (!isOnSyncQueue(node)) {
 8                 LockSupport.park(this);
 9                 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10                     break;
11             }
12             if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13                 interruptMode = REINTERRUPT;
14             if (node.nextWaiter != null) // clean up if cancelled
15                 unlinkCancelledWaiters();
16             if (interruptMode != 0)
17                 reportInterruptAfterWait(interruptMode);
18         }

    我來講解下上面的方法:

addConditionWaiter:將當前條件隊列從后遍歷進行刪除已經沒有用的節點,並且將當前線程添加到條件隊列當中;返回當前節點。
fullyRelease:將當前線程的鎖全部釋放掉,並且當前的獨占線程置位null后,喚醒隊列的頭節點,但是目前我們的隊列還沒有任何阻塞節點,所以只是釋放了鎖。
isOnSyncQueue:查看是否當前節點已經在同步隊列中。
checkInterruptWhileWaiting:查看點前節點是否被中斷,如果沒有則將當前節點添加到同步隊列當中。
acquireQueued:同步隊列中頭節點重新獲取鎖並返回。
unlinkCancelledWaiters:刪除條件隊列中被中斷的節點。
reportInterruptAfterWait:中斷當前線程。

  我們不難發現,當前節點沒有添加成功會先添加到條件隊列中,然后釋放持有的獨占鎖,並且判斷是否已經加入到了同步隊列中去,沒有的話線程阻塞到這里。一旦被釋放就會

立即添加到同步隊列中。然后做一些后續處理。

 1 public E take() throws InterruptedException {
 2         final ReentrantLock lock = this.lock;
 3         lock.lockInterruptibly();
 4         try {
 5             while (count == 0)
 6                 notEmpty.await();
 7             return dequeue();
 8         } finally {
 9             lock.unlock();
10         }
11     }

  這里的await跟上面的步驟是一樣的,只不過他會釋放上一個阻塞的線程,讓它添加到同步隊列中。

 ps:關注一下本人公眾號,每周都有新更新哦!





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM