Java多線程中Lock的實現

在Java 1.5之後,併發包中新增了Lock接口用來實現鎖功能,它提供了Synchronized關鍵字類似的功能,只是在使用時需要顯式地獲取鎖和釋放鎖。雖然它缺少了隱式獲取鎖釋放鎖的便捷性,但是卻擁有了鎖釋放和獲取的可操作性、可中斷地獲取鎖以及超時獲取鎖等多種選擇。

1 Lock接口

Lock接口的主要api如下:

1)void lock():獲取鎖,調用該方法的當前線程或獲取鎖,並從該方法返回,沒有獲取鎖的將會自旋等待。

2)void lockInterruptibly () throws InterruptedException:可中斷的獲取鎖,該方法會響應中斷,即在獲取鎖的自旋等待中,可以中斷當前線程。

3)boolean tryLock():嘗試非阻塞的獲取鎖,調用該方法會立刻返回,獲取則返回true。

4)Condition newCondition():返回等待通知組件,類似於Object.wait與Object.notify方法,可以使線程進入waiting狀態。

5)相對應的unlock()方法。

在Lock的具體實現中,基本都是通過聚合了一個隊列同步器(AbstractQueuedSynchronizer,AQS)的子類來完成線程訪問控制的。下面我們將詳細介紹AQS的使用和實現。

2 隊列同步器AQS

隊列同步器(AbstractQueuedSynchronizer,AQS)是構建鎖或者其他同步組件的基礎框架,它使用了一個int state變量來表示同步狀態。通過內置的FIFO(先進先出)的同步隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承。子類通過對同步器的繼承並實現它的抽象方法來管理同步狀態。同時,它提供了三個方法(getState()、setState(int newState)和compareAndSetState(int expect , int update))來對同步狀態進行更改,它們能夠保證操作的安全性。同步器支持獨佔式和共享式地獲取同步狀態。

2.1 同步器的接口

同步器的設計是基於模板方法模式的,我們需要重寫指定的方法,隨後將同步器指定在自定義同步組件中,並調用同步器提供的模板方法,這些模板方法會調用使用者重寫的方法來實現特定的同步規則。可重寫的方法如下:
1)boolean tryAcquire(int arg):獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設置同步狀態。
2)boolean tryRelease(int arg):獨佔鎖釋放同步狀態,等待同步狀態的線程將有機會獲取同步狀態。
3)int tryAcquireShared(int arg):共享式獲取同步,返回大於0表示獲取成功。
4)boolean tryRealeaseShared(int arg):共享是鎖釋放
5)boolean isHeldExclusively():當前線程同步器是否被該線程獨佔。 

上述的幾個方法將被模板方法引用,我們在實現自定義方法時,將調用這些模板方法。模板方法如下:

1)void acquire(ing arg):獨佔式獲取同步鎖(調用tryAcquire()),成功的話,則同步器的鎖被該線程擁有(同步器記錄下該線程),方法返回。否則進入同步隊列等待。

2)void acquireInterruptibly(int arg):與1)相同,但是能響應中斷,在同步隊列等待時,可以響應中斷,拋出異常並返回。

3)boolean AcquireShared(int arg):共享式的獲取鎖。

4)Collection<Thread> getQueuedThreads():獲取等待在同步隊列上的線程。

5)其他獨佔、共享、中斷的組合獲取鎖和釋放鎖的方法。

2.2 模板方法的實現

1.同步隊列

同步器依賴內部的同步隊列(FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點(NODE)並將其加入同步隊列,同時會阻塞當前線程(自旋,不是系統級的線程阻塞),當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態(公平鎖)。未取得鎖的線程將被構造成節點加入同步隊列尾端。

2.獨佔式同步狀態獲取與釋放

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是線程失敗後會進入同步隊列,後續線程進行中斷操作時,線程不會從同步隊列中移除。方法代碼如下:
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
上述代碼主要完成了同步狀態的獲取、節點構造、加入同步隊列和在同步隊列中自旋等操作。首先調用tryAcquire()方法獲取同步,如果失敗,則使用addWaiter構造節點並加入同步隊列。最後用acquireQueued方法,使節點自旋來獲取同步鎖。
tryAcquire()方法是需要我們重寫的,下面展示一個簡單的不可重入鎖的tryAcquire()的例子:
	public boolean tryAcquire(int acquires){
		if(this.compareAndSetState(0, 1)){	//如果鎖未被佔用,則設置爲1
			this.setExclusiveOwnerThread(Thread.currentThread());	//將此線程設爲鎖的擁有者
			return true;
		}
		return false;
	}
在這個方法中,同步器的狀態只能爲0和1,同樣的線程不能重新入鎖,因爲加鎖後狀態爲1,方法中只有0纔可以入鎖。同一個線程,如果擁有鎖後繼續入鎖,則會被加入同步隊列,發生阻塞。這是一個“不好”的設計。
那麼,我們常用的ReentrantLock不公平重入鎖是怎麼設計的,請看下面的代碼:
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        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;
        }

可以看到,當狀態爲0時,會同上面的不可重入鎖一樣進行加鎖並取得鎖。但如果不爲0,則對線程進行判斷,如果當前線程是擁有鎖的線程,則可以正確返回,執行同步方法。否則,將加入同步隊列等待。
在acquireQueued()方法中,線程會進入死循環,一直嘗試獲取同步狀態,線程會先判斷自己是不是首節點,如果是,則會嘗試獲取鎖。否則留在自旋中。代碼如下:
    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())	//會調用LockSupport.park()方法阻塞自己,等待前繼節點喚醒自己
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

自旋獲取鎖的過程:

判斷當前節點的前驅節點。

需要獲取當前節點的前驅節點的狀態,當前驅節點是頭結點並且當前節點(線程)能夠獲取狀態(tryAcquire方法成功),

代表該當前節點佔有鎖,如果滿足上述條件,那麼代表能夠佔有鎖,根據節點對鎖佔有的含義,設置頭結點爲當前節點(setHead)。

如果沒有滿足上述條件,判斷前一個節點的狀態,並調用parkAndCheckInterrupt方法使得當前線程阻塞,直到unpark調用(前一個節點釋放鎖的時候會通知後繼節點),Thread的interrupt調用,然後重新輪訓去嘗試上述操作。

在當前線程獲取同步並執行完成後,就會釋放同步狀態,方法如下:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
該方法會喚醒頭結點的後續節點。

3 共享式同步狀態的獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。其獲取鎖的代碼如下:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)	
            doAcquireShared(arg);
    }
    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);
        }
    }

在acquireShared()放法中,先會tryAcquireShared(arg),如果大於0成功,則獲取同步狀態。否則,加入同步隊列,阻塞,等待前繼節點執行完同步方法後喚醒,重新嘗試獲取同步狀態。共享鎖的釋放同獨佔鎖,需要喚醒後繼節點。

4 獨佔式超時獲取同步狀態

可以在指定時間內獲取同步狀態,如果獲取成功,則返回true,否則,返回false。在acquireSharedInterruptibly()的基礎是增加了時間限制。不再詳述。


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