【Java併發編程實戰】-----“J.U.C”:ReentrantLock之二lock方法分析

前一篇博客簡單介紹了ReentrantLock的定義和與synchronized的區別,下面跟隨LZ的筆記來扒扒ReentrantLock的lock方法。我們知道ReentrantLock有公平鎖、非公平鎖之分,所以lock()我也已公平鎖、非公平鎖來進行闡述。首先我們來看ReentrantLock的結構【圖來自Java多線程系列--“JUC鎖”03之 公平鎖(一)】:

2015073100001

從上圖我們可以看到,ReentrantLock實現Lock接口,Sync與ReentrantLock是組合關係,且FairSync(公平鎖)、NonfairySync(非公平鎖)是Sync的子類。Sync繼承AQS(AbstractQueuedSynchronizer)。在具體分析lock時,我們需要了解幾個概念:

AQS(AbstractQueuedSynchronizer):爲java中管理鎖的抽象類。該類爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。該類提供了一個非常重要的機制,在JDK API中是這樣描述的:爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。此類的設計目標是成爲依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。子類必須定義更改此狀態的受保護方法,並定義哪種狀態對於此對象意味着被獲取或被釋放。假定這些條件之後,此類中的其他方法就可以實現所有排隊和阻塞機制。子類可以維護其他狀態字段,但只是爲了獲得同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操作以原子方式更新的 int 值。 這麼長的話用一句話概括就是:維護鎖的當前狀態和線程等待列表。

CLH:AQS中“等待鎖”的線程隊列。我們知道在多線程環境中我們爲了保護資源的安全性常使用鎖將其保護起來,同一時刻只能有一個線程能夠訪問,其餘線程則需要等待,CLH就是管理這些等待鎖的隊列。

CAS(compare and swap):比較並交換函數,它是原子操作函數,也就是說所有通過CAS操作的數據都是以原子方式進行的。

公平鎖(FairSync):lock

lock()定義如下:

final void lock() {
            acquire(1);
        }

lock()內部調用acquire(1),爲何是”1”呢?首先我們知道ReentrantLock是獨佔鎖,1表示的是鎖的狀態state。對於獨佔鎖而言,如果所處於可獲取狀態,其狀態爲0,當鎖初次被線程獲取時狀態變成1。

acquire()是AbstractQueuedSynchronizer中的方法,其源碼如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

從該方法的實現中我們可以看出,它做了非常多的工作,具體工作我們先晾着,先看這些方法的實現:

tryAcquire

tryAcquire方法是在FairySync中實現的,其源代碼如下:

複製代碼
protected final boolean tryAcquire(int acquires) {
        //當前線程
        final Thread current = Thread.currentThread();
        //獲取鎖狀態state
        int c = getState();
        /*
         * 當c==0表示鎖沒有被任何線程佔用,在該代碼塊中主要做如下幾個動作:
         * 則判斷“當前線程”是不是CLH隊列中的第一個線程線程(hasQueuedPredecessors),
         * 若是的話,則獲取該鎖,設置鎖的狀態(compareAndSetState),
         * 並切設置鎖的擁有者爲“當前線程”(setExclusiveOwnerThread)。
         */
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        /*
         * 如果c != 0,表示該鎖已經被線程佔有,則判斷該鎖是否是當前線程佔有,若是設置state,否則直接返回false
         */
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
複製代碼

在這裏我們可以肯定tryAcquire主要是去嘗試獲取鎖,獲取成功則設置鎖狀態並返回true,否則返回false。

hasQueuedPredecessors:"當前線程"是不是在CLH隊列的隊首,來返回AQS中是不是有比“當前線程”等待更久的線程(公平鎖)。

複製代碼
public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;    
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
複製代碼

Node是AbstractQueuedSynchronizer的內部類,它代表着CLH列表的一個線程節點。對於Node以後LZ會詳細闡述的。

compareAndSetState:設置鎖狀態

protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }

compareAndSwapInt() 是sun.misc.Unsafe類中的一個本地方法。對此,我們需要了解的是 compareAndSetState(expect, update) 是以原子的方式操作當前線程;若當前線程的狀態爲expect,則設置它的狀態爲update。

setExclusiveOwnerThread:設置當前線程爲鎖的擁有者

protected final void setExclusiveOwnerThread(Thread t) {
        exclusiveOwnerThread = t;
    }

addWaiter(Node.EXCLUSIVE)

複製代碼
private Node addWaiter(Node mode) {
        //new 一個Node節點
        Node node = new Node(Thread.currentThread(), mode);
        
        //CLH隊列尾節點
        Node pred = tail;
        
        //CLH尾節點!= null,表示CLH隊列 != null,則將線程加入到CLH隊列隊尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //若CLH隊列爲空,則調用enq()新建CLH隊列,然後再將“當前線程”添加到CLH隊列中。
        enq(node);
        return node;
    }
複製代碼

addWaiter()主要是將當前線程加入到CLH隊列隊尾。其中compareAndSetTail和enq的源代碼如下:

複製代碼
/**
     * 判斷CLH隊列的隊尾是不是爲expect,是的話,就將隊尾設爲update
     * @param expect
     * @param update
     * @return
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    
    /**
     * 如果CLH隊列爲空,則新建一個CLH表頭;然後將node添加到CLH末尾。否則,直接將node添加到CLH末尾
     * @param node
     * @return
     */
    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;
                }
            }
        }
    }
複製代碼

addWaiter的實現比較簡單且實現功能明瞭:當前線程加入到CLH隊列隊尾。

acquireQueued

複製代碼
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //線程中斷標誌位
            boolean interrupted = false;
            for (;;) {
                //上一個節點,因爲node相當於當前線程,所以上一個節點表示“上一個等待鎖的線程”
                final Node p = node.predecessor();
                /*
                 * 如果當前線程是head的直接後繼則嘗試獲取鎖
                 * 這裏不會和等待隊列中其它線程發生競爭,但會和嘗試獲取鎖且尚未進入等待隊列的線程發生競爭。這是非公平鎖和公平鎖的一個重要區別。
                 */
                if (p == head && tryAcquire(arg)) {
                    setHead(node);     //將當前節點設置設置爲頭結點
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                /* 如果不是head直接後繼或獲取鎖失敗,則檢查是否要阻塞當前線程,是則阻塞當前線程
                 * shouldParkAfterFailedAcquire:判斷“當前線程”是否需要阻塞
                 * parkAndCheckInterrupt:阻塞當前線程
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);     
        }
    }
複製代碼

在這個for循環中,LZ不是很明白爲什麼要加p==head,Java多線程系列--“JUC鎖”03之 公平鎖(一)這篇博客有一個較好的解釋如下:

p == head && tryAcquire(arg) 
首先,判斷“前繼節點”是不是CHL表頭。如果是的話,則通過tryAcquire()嘗試獲取鎖。 
其實,這樣做的目的是爲了“讓當前線程獲取鎖”,但是爲什麼需要先判斷p==head呢?理解這個對理解“公平鎖”的機制很重要,因爲這麼做的原因就是爲了保證公平性! 
      (a) 前面,我們在shouldParkAfterFailedAcquire()我們判斷“當前線程”是否需要阻塞; 
      (b) 接着,“當前線程”阻塞的話,會調用parkAndCheckInterrupt()來阻塞線程。當線程被解除阻塞的時候,我們會返回線程的中斷狀態。而線程被解決阻塞,可能是由於“線程被中斷”,也可能是由於“其它線程調用了該線程的unpark()函數”。 
      (c) 再回到p==head這裏。如果當前線程是因爲其它線程調用了unpark()函數而被喚醒,那麼喚醒它的線程,應該是它的前繼節點所對應的線程(關於這一點,後面在“釋放鎖”的過程中會看到)。 OK,是前繼節點調用unpark()喚醒了當前線程! 
此時,再來理解p==head就很簡單了:當前繼節點是CLH隊列的頭節點,並且它釋放鎖之後;就輪到當前節點獲取鎖了。然後,當前節點通過tryAcquire()獲取鎖;獲取成功的話,通過setHead(node)設置當前節點爲頭節點,並返回。 
       總之,如果“前繼節點調用unpark()喚醒了當前線程”並且“前繼節點是CLH表頭”,此時就是滿足p==head,也就是符合公平性原則的。否則,如果當前線程是因爲“線程被中斷”而喚醒,那麼顯然就不是公平了。這就是爲什麼說p==head就是保證公平性!

在該方法中有兩個方法比較重要,shouldParkAfterFailedAcquire和parkAndCheckInterrupt,其中

shouldParkAfterFailedAcquire:判斷“當前線程”是否需要阻塞,源碼如下:

複製代碼
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;
        }
複製代碼

waitStatus是節點Node定義的,她是標識線程的等待狀態,他主要有如下四個值:

CANCELLED = 1:線程已被取消;

SIGNAL = -1:當前線程的後繼線程需要被unpark(喚醒);

CONDITION = -2 :線程(處在Condition休眠狀態)在等待Condition喚醒;

PROPAGATE = –3:(共享鎖)其它線程獲取到“共享鎖”.

有了這四個狀態,我們再來分析上面代碼,當ws == SIGNAL時表明當前節點需要unpark(喚醒),直接返回true,當ws > 0 (CANCELLED),表明當前節點已經被取消了,則通過回溯的方法(do{}while())向前找到一個非CANCELLED的節點並返回false。其他情況則設置該節點爲SIGNAL狀態。我們再回到if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),p是當前節點的前繼節點,當該前繼節點狀態爲SIGNAL時返回true,表示當前線程需要阻塞,則調用parkAndCheckInterrupt()阻塞當前線程。

parkAndCheckInterrupt:阻塞當前線程,並且返回“線程被喚醒之後”的中斷狀態,源碼如下:

private final boolean parkAndCheckInterrupt() {
    //通過LockSupport的park()阻塞“當前線程”。
        LockSupport.park(this);
        return Thread.interrupted();
    }

從上面我們可以總結,acquireQueued()是當前線程會根據公平性原則來進行阻塞等待,直到獲取鎖爲止;並且返回當前線程在等待過程中有沒有並中斷過。

selfInterrupt

private static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
selfInterrupt()產生一箇中斷。如果在acquireQueued()中當前線程被中斷過,則需要產生一箇中斷。

Fairy lock()總結

我們再看acquire()源碼:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先通過tryAcquire方法嘗試獲取鎖,如果成功直接返回,否則通過acquireQueued()再次獲取。在acquireQueued()中會先通過addWaiter將當前線程加入到CLH隊列的隊尾,在CLH隊列中等待。在等待過程中線程處於休眠狀態,直到成功獲取鎖纔會返回。如下:

2015081100001

非公平鎖(NonfairSync):lock

非公平鎖NonfairSync的lock()與公平鎖的lock()在獲取鎖的流程上是一直的,但是由於它是非公平的,所以獲取鎖機制還是有點不同。通過前面我們瞭解到公平鎖在獲取鎖時採用的是公平策略(CLH隊列),而非公平鎖則採用非公平策略它無視等待隊列,直接嘗試獲取。如下:

複製代碼
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
複製代碼

lock()通過compareAndSetState嘗試設置所狀態,若成功直接將鎖的擁有者設置爲當前線程(簡單粗暴),否則調用acquire()嘗試獲取鎖;

acquire

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

在非公平鎖中acquire()的實現和公平鎖一模一樣,但是他們嘗試獲取鎖的機制不同(也就是tryAcquire()的實現不同)。

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

tryAcquire內部調用nonfairyTryAcquire:

複製代碼
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製代碼

與公平鎖相比,非公平鎖的不同之處就體現在if(c==0)的條件代碼塊中:

複製代碼
//----------------非公平鎖-----  
  if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
  //----------------公平鎖-----  
 if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
複製代碼
是否已經發現了不同之處。公平鎖中要通過hasQueuedPredecessors()來判斷該線程是否位於CLH隊列中頭部,是則獲取鎖;而非公平鎖則不管你在哪個位置都直接獲取鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章