AbstractQueuedSynchronizer(AQS)源碼解析

      關於AQS的源碼解析,原本是沒有打算特地寫一篇文章來介紹的。不過在寫本學期課程做業中,有一門寫了關於AQS的,並且也畫了一些相關的圖,因此直接拿過來分享一下,若有錯誤歡迎指正。
      而後基本簡介也都不介紹了,網上一大堆,這裏就直接進行源碼的分析了。java

AQS基本屬性

      
      AQS屬性簡介:node

屬性 類型 詳解
Head Node類型 持有鎖的線程結點,也是隊列中的頭結點
Tail Node類型 阻塞隊列中的尾結點,同時每個新的結點進來,都插入到阻塞隊列的最後。
State int類型 大於等於0。表明當前鎖的狀態。0表明沒有線程佔用當前鎖,大於0表明有線程持有鎖。
exclusiveOwnerThread(繼承自AOS) Thread類型 表明獨佔鎖的線程。

      AQS的具體結構以下圖所示:
ui

      在AQS鏈表中,將每個線程包裝成Node實例,並經過鏈表的形式連接保存,在鏈式結構中,節點經過next和prev分別與前驅節點和後置節點相鏈接。其中head節點表示爲當前持有鎖的線程,不在阻塞隊列中。tail節點爲鏈表中最後一個節點,當有新的節點被添加到鏈表中後,AQS會將tail引用指向最後一個被添加進鏈表的節點。this

AQS中Node內部類

      
      Node屬性簡介:線程

字段 簡介 字段 簡介
SHARE 標識節點當前在共享模式下 EXCLUSIVE 標識節點當前在獨佔模式下
CANCELLED 標識當前節點所表示的線程已取消搶鎖 SIGNAL 標識當前節點須要在釋放鎖後喚醒後繼節點
CONDITION 與ConditionObject內部類有關 waitStatue 取值爲以上幾種狀態
prev 表明當前節點的前驅節點 next 表明當前節點的後繼節點
thread 表明當前節點所表示的線程

1 加鎖

      這裏以一個鎖的具體使用方法對AQS類進行詳細的分析:
code

      首先,線程先對鎖對象進行獲取操做,若是當前須要獲取的鎖對象並無其餘線程所持有,成功獲取到了鎖,將執行相關的業務代碼,執行完畢後,對鎖資源進行釋放,以便其餘線程所使用。若是當前線程獲取鎖資源失敗,說明鎖資源有其餘線程在使用,當前線程將進行阻塞狀態,等待再次獲取鎖資源。對象

1.1 AQS中如何獲取鎖

java.util.concurrent.locks.ReentrantLock.java文件中的公平鎖爲例:blog

abstract static class Sync extends AbstractQueuedSynchronizer
#java.util.concurrent.locks.ReentrantLock中第220行
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);  #調用了AQS中的方法
        }
...
}
================AQS====================
#java.util.concurrent.locks.AbstractQueuedSynchronizer中第1197行
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

      在lock()方法中,線程首先嚐試搶鎖tryAcquire(1),若是搶鎖成功則直接返回true,表明當前線程已持有鎖資源,不然返回false,進行下一次搶鎖動做。
      當線程搶鎖失敗後,AQS將將當前線程封裝成Node節點,並添加到阻塞隊列。以後將從阻塞隊列中依次取出等待鎖的Node節點,並再次嘗試獲取鎖.若是再次獲取鎖失敗,則使當前線程本身中斷本身。繼承

1.2 嘗試獲取鎖資源

      首先獲取鎖的狀態,判斷當前是否有線程持有鎖,這裏分爲兩種狀況:隊列

  • 若是當前並無線程持有鎖資源,則判斷阻塞隊列中是否有節點排在當前節點的前面等待獲取鎖資源。這裏分爲兩種狀況:

    • 若是有其餘線程在等待獲取鎖資源,則進行等待
    • 若是沒有其餘線程在等待獲取鎖資源,代表當前線程是第一個等待獲取鎖的線程,隨後嘗試對鎖資源進行獲取,若是成功獲取到鎖資源則將當前線程設置爲獨佔鎖的線程,同時返回true.
  • 若是當前有線程持有鎖,則進行判斷是不是當前線程所持有鎖資源,這是分爲兩種狀況:
    • 鎖資源被當前線程所持有,則代表是重入鎖,隨後將獲取鎖的次數加一,返回true.
    • 持有鎖資源並非當前線程,返回false.

流程圖以下:

源碼:

#java.util.concurrent.locks.ReentrantLock中第231行
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
        }
        return false;
}

1.3 判斷阻塞隊列中是否有其餘節點

      在線程獲取鎖以前,首先判斷阻塞隊列中是否有其餘節點,若是有其餘節點則放棄搶鎖。
首先獲取AQS鏈表中的頭節點與尾節點,分別進行判斷:

  • 頭節點是否等於尾節點
    • 若是頭節點等於尾節點說明阻塞隊列爲空,沒有其餘節點返回false
  • 若是頭節點不等於尾節點,則判斷頭節點的後置節點是否爲空
    • 若是頭節點的後置節點不爲空,則說明阻塞隊列不爲空,則判斷阻塞隊列中第一個節點線程是否爲當前線程
      • 若是是當前線程說明阻塞隊列中沒有其餘節點返回false。
      • 若是不是當前線程說明阻塞隊列中有其餘節點,返回true.

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第1512行
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

1.4 將當前線程添加到阻塞隊列

      若是當前線程搶鎖失敗則經過AQS將當前線程包裝成Node節點添加進阻塞隊列。

  1. 將當前線程以獨佔鎖的模式包裝成Node節點。

  2. 將當前節點添加進阻塞隊列。分兩種狀況:
    • 阻塞隊列中尾節點不爲空。
      • 將尾節點置爲當前節點的前驅節點,經過CAS操做將當前節點置爲尾節點。
        • 若是成功,則將以前尾結點的後置引用指向當前節點,將當前節點返回。
        • 若是存在另外一節點提早完成上一步操做,則進行入隊操做。
    • 阻塞隊列中尾節點爲空,則進行入隊操做。
  3. 入隊操做結束將當前節點返回。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第605行
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

1.5 入隊操做

      這一步將當前節點添加到阻塞隊列中。
      首先獲取阻塞隊列中的尾節點,判斷是否爲空,有兩種狀況:

  • 阻塞隊列中尾節點爲空,則初始化阻塞隊列,將頭節點設置爲尾節點,
    再次獲取尾節點,判斷是否爲空。
  • 阻塞隊列中尾節點不爲空,則將尾節點設置爲當前節點的前驅節點。
    經過CAS將當前節點設置爲尾節點。這裏有兩種狀況:
    • 若是成功,則將以前尾結點的後置引用指向當前節點,將當前節點的前驅節點返回。
    • 存在另外一節點提早完成上一步操做,則再次獲取阻塞隊列中的尾節點,判斷是否爲空。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第583行
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

1.6 搶鎖或將線程掛起

      到達這一步說明節點已進入阻塞隊列,節點嘗試獲取鎖或者進行掛起操做。

  1. 獲取當前節點的前驅節點
  2. 判斷前驅節點是否爲頭節點,這裏有兩種狀況:
    • 前驅節點爲頭節點,說明當前節點前面沒有節點在等待獲取鎖資源,只須要等待前驅節點釋放鎖資源。因此能夠嘗試搶鎖,這裏有兩種狀況:
      • 搶鎖成功,則將當前節點設置爲頭節點,將當前節點前驅節點的後置引用設置爲空,返回false
      • 搶鎖失敗,說明頭節點尚未釋放鎖資源,此時將當前節點掛起。這裏有兩種狀況:
        • 若是掛起成功,則線程等待被喚醒,喚醒以後再次判斷前驅節點是否爲頭節點。
        • 若是掛起失敗,再次判斷前驅節點是否爲頭節點。
    • 前驅節點不是頭節點,說明當前節點前面有其餘節點在等待獲取鎖資源,此時將當前節點掛起。
  3. 若是在掛起階段發生異常,則取消搶鎖。
  4. 這裏爲無限循環,直到線程獲取到鎖資源或者取消搶鎖纔會退出循環。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第857行
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
            }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

1.7 判斷是否應該掛起當前線程

      當線程暫時獲取不到鎖資源時,判斷是否應該掛起當前線程。
      首先獲取當前節點的前驅節點的狀態,這裏有三種狀況:
* 前驅節點的狀態爲SIGNAL。其中,SIGNAL代表該節點在釋放鎖資源後應該將後置節點喚醒。返回true。
* 前驅節點的狀態爲CANCELLED。CANCELLED代表該節點已取消搶鎖,此時將從當前節點開始向前尋找,直到找到一個節點的狀態不爲CANCELLED,而後將他設置爲當前節點的前驅節點。以後返回false.
* 若是前驅節點的狀態不是以上兩種狀況,則經過CAS將前驅節點的狀態設置爲SIGNAL,以後返回false。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第795行
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

1.8 掛起當前線程

      將當前線程掛起,當線程被喚醒後將線程的中斷狀態返回.
源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第835行
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

2 解鎖

2.1 解鎖操做

      嘗試釋放鎖資源,這裏有兩種狀況:

  • 成功釋放鎖資源,獲取到AQS鏈表中頭節點,判斷頭節點是否爲空,這裏有兩種狀況:
    • 若是頭節點爲空,說明沒有節點持有鎖資源,返回true.
    • 若是頭節點不爲空,判斷頭節點狀態是否爲0:
      • 若是頭節點狀態爲0,說明阻塞隊列中沒有線程在等待獲取鎖,返回true.
      • 若是頭節點狀態不爲0,則將阻塞隊列中第一個等待獲取鎖資源的線程喚醒。隨後返回ture.

流程圖以下:

源碼:

#java.util.concurrent.locks.ReentrantLock中第456行
public void unlock() {
    sync.release(1);
}
==============================
#java.util.concurrent.locks.AbstractQueuedSynchronizer中第1260行
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

2.2 喚醒後置節點

      當持有鎖的節點執行相關代碼完成後,須要釋放鎖資源並喚醒後置節點。

  1. 首先獲取頭節點的狀態,若是小於0則經過CAS將狀態設置爲0.
  2. 獲取頭節點的後置節點,這裏有兩種狀況:
    • 若是頭節點的後置節點爲空或者頭節點的後置節點的狀態大於0,則將頭節點的後置節點置爲空,同時從AQS鏈表的尾節點向前搜索,直到找到最後一個節點狀態小於等於0的節點,將該節點喚醒。
    • 若是頭節點的後置節點不爲空,則直接將該節點喚醒。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第638行
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

2.3 取消搶鎖

      當線程因爲異常或某些特殊狀況的發生,須要取消對鎖資源的獲取,將執行取消搶鎖操做。

  1. 若是須要取消搶鎖的節點爲空,則直接返回。
  2. 不然將節點所包裝的線程置爲空。
  3. 獲取節點的前驅節點,判斷前驅節點的狀態是否大於0,若是大於0則一直向前找,直到找到一個節點的狀態小於等於0,將該節點設置爲當前節點的前驅節點。
  4. 獲取當前節點的後置節點。
  5. 將當前節點的狀態設置爲CANCELLED。
  6. 判斷當前節點是否爲尾節點,這裏有兩種狀況:
    • 若是當前節點是尾節點,則將當前節點的前驅節點設置爲尾節點,
      同時將後置引用設置爲空。
    • 若是當前節點不是尾節點,判斷當前節點的前驅節點是否爲頭節
      點。這裏有兩種狀況:
      • 若是當前節點的前驅節點是頭節點,則將當前節點喚醒。
      • 若是當前節點的前驅節點不是頭節點,則判斷該節點狀態是否爲SIGNAL,若是爲SIGNAL,則將該節點的後置引用指向當前節點的後置節點。
    • 斷開當前節點與鏈表的鏈接。

流程圖以下:

源碼:

#java.util.concurrent.locks.AbstractQueuedSynchronizer中第742行
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    
    node.thread = null;

    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    node.waitStatus = Node.CANCELLED;

    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
            (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
            compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

      其實到這裏還有一些內容並無分析完,之後再補上好了。

相關文章
相關標籤/搜索