Java併發---concurrent包

xiang發表於2020-08-24

一、包的結構層次

 

 其中包含了兩個子包atomic和locks,另外字concurrent下的阻塞佇列以及executor,這些就是concurrent包中的精華。而這些類的實現主要是依賴於volatile和CAS,從整體上看concurrent包的整體實現圖如下:

二、Lock和synchronized的比較

鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源。在Lock介面出現之前,Java主要是靠synchronized關鍵字實現鎖功能的,而Java1.5之後,併發包增加了Lock介面,它提供了與synchronized一樣的鎖功能。雖然它失去了想synchronized關鍵字隱式加鎖解鎖的便捷性,單卻擁有了獲取鎖和釋放鎖的可操作性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性

通常Lock使用的形式如下:

Lock lock=new ReentrantLock();
lock.lock();
try {
            
} finally {
    lock.unlock();
}

需要注意的是:synchronized同步塊執行完成活著遇到異常會自動釋放鎖,而lock必須呼叫unlock()釋放鎖,因此必須在finally中釋放鎖。

三、Lock介面API的介紹

看看Lock定義了哪些方法:

void lock();獲取鎖

void lockInterruptibly() throws InterruptedException;獲取所的過程能夠響應中斷

boolean tryLock();非阻塞式響應中斷能立即返回,獲取鎖返回true,反之返回false;

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;超時獲取鎖,在超時內或者中斷的情況下能夠獲取鎖

Condition newCondition();獲取與lock繫結的等待通知元件,當前執行緒必須獲得了鎖才能進行等待,進行等待時會先釋放鎖,當再次獲取鎖時才能從等待中返回;

void unlock();釋放鎖

那麼在locks包下有哪些類實現了改介面?

ReentrantLock

public class ReentrantLock implements Lock, java.io.Serializable 

很顯然ReentrantLock實現了lock介面,當你檢視原始碼是會發現ReentrantLock並沒有多少程式碼,另外一個很明顯的特點是:基本上所有的方法的實現實際上都是呼叫了其靜態內部類Sync中的方法,而Sync類繼承了AbstractQueuedSynchronizer(AQS)。可以看出ReentrantLock關鍵核心在於對佇列同步器AbstractQueuedSynchronizer的理解。

四、瞭解佇列同步器AQS(AbstractQueuedSynchronizer)

關於AQS在原始碼中有十分具體的解釋

 同步器是用來構建鎖和其他同步元件的基礎框架,它的實現主要依賴一個int成員變數來表示同步狀態以及通過一個FIFO佇列構成等待佇列。它的子類必須重寫AQS的幾個protected修飾的用來改變同步狀態的方法,其他方法主要是實現了排隊和阻塞機制。狀態的更新使用getState,setState以及compareAndSetState這三個方法。

子類被推薦定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態的獲取和釋放方法來供自定義同步元件的使用,同步器既支援獨佔式獲取同步狀態,也支援共享式獲取同步狀態,這樣就可以方便的實現不同型別的同步元件。

同步器是實現鎖(也可以是任意同步元件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者的關係:鎖是面向使用者,它定義了使用者與鎖互動的介面,隱藏了實現細節;同步器是面向鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態的管理,執行緒的排隊,等待和喚醒等底層操作。鎖和同步器很好的隔離了使用者和實現者所需要關注的領域。

 五、AQS的設計模式

AQS的設計模式是使用模板方法設計模式,它將一些方法開放給子類進行重寫,而同步器給同步元件所提供模板方法又會重新被子類重寫的方法。

舉個例子,AQS中需要重寫的方法tryAcquire

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

ReentrantLock中NonfairSync(繼承AQS)會重寫該方法為:

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

而AQS中的模板方法acquire():

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

會呼叫tryAcquire方法,而此時當繼承AQS的NonfairSync呼叫模板方法acquire時就會呼叫已經被NonfairSync重寫的tryAcquire方法。這就是使用AQS的方式。

同步元件(這裡不僅僅指鎖,還包括CountDownLatch等)的實現依賴於同步器AQS,在同步元件實現中,使用AQS的方式被推薦定義繼承AQS的靜態內部類;

AQS採用模板方法進行設計,AQS的protected修飾的方法需要由繼承AQS的子類進行重寫實現,當呼叫AQS的子類的方法是就會呼叫被重寫的方法;

AQS負責同步狀態的管理,執行緒的排隊,等待和喚醒這些底層操作,而Lock等同步元件主要專注於實現同步語義;

在重寫AQS的方式時,使用AQS提供的getState(),setState()以及compareAndSetState()方法進行修改同步狀態.

AQS可重寫的方法如下圖:

 

 在實現同步元件是AQS提供的模板方法如下圖

 

 AQS提供的模板方法可以分為3類:

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

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

查詢同步佇列中等待執行緒情況

同步元件通過AQS提供的模板方法實現自己的同步語義。

 六、AQS的使用example

class Mutex implements Lock, java.io.Serializable {

    // Our internal helper class
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}
package passtra;

public class MutextDemo {
    private static Mutex mutex = new Mutex();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                mutex.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}

這個例子來源於AQS原始碼的example,執行情況:

 上面的例子實現了獨佔鎖的語義,在同一個時刻只允許一個執行緒佔有鎖。 MutextDemo新建10個執行緒,分別睡眠3s。從執行情況也可以看出當前Thread-6正在執行佔有鎖而其他執行緒7/8處於WAIT狀態。按照推薦的方式,Mutext定義了一個整合AQS的靜態內部類Sync,並且重寫了AQS的tryAcquire等等方法,而對state的更新也是利用了getState,setState,compareAndSetStaste這三個方法。在實現lock介面中方法也是呼叫AQS提供的模板方法(因為Sync繼承了AQS)。

從這個例子就可以很清楚的看出來,在同步元件的實現上主要利用了AQS,而AQS“遮蔽”了同步狀態的修改,執行緒排隊等底層實現,通過AQS的模板方法可以很方便的給同步元件的實現警醒呼叫。而針對使用者來說,只需要呼叫同步元件提供的方法來實現併發程式設計即可,同時在新建一個同步元件時需要把握的兩個關鍵點是:

實現同步元件時推薦定義繼承AQS的靜態內部類,並重寫需要的proyected修飾的方法;

同步元件語義的實現依賴於AQS的模板方法,而AQS模板方法有依賴於AQS的子類重寫的方法。

 通俗的說,因為AQS整體設計思路採用模板方法設計模式,同步元件以及AQS的功能實際上分別切換成各自的兩部分:

同步元件實現者的角度:

通過可重新寫的方法:

獨佔式:

tryAcquire()(獨佔式獲取同步狀態);

tryRelease()(獨佔式釋放同步狀態)

共享式:

tryAcquireShared()(共享式獲取同步狀態)

tryReleaseShared()(共享式釋放同步狀態

告訴AQS怎麼判斷當前同步狀態是否成功獲取或者是否成功釋放。

同步元件專注於對當前同步狀態的邏輯判斷,從而實現自己的同步語義。這句話比較抽象,舉個例子,上面的Mutex例子中通過tryAcquire方法實現自己的同步語義,在該方法中如果當前同步狀態為0(即該同步元件沒有被任何執行緒獲取),當前執行緒可以獲取同時將狀態更改為1返回true,否則,該元件只能在同一時刻被執行緒佔用,mutex專注於獲取釋放的邏輯來實現自己想要表達的同步語義。

AQS角度

而對AQS來說,只需要同步元件返回的true和false即可,因為AQS會對true和fals會有不同的操作,true會認為當前執行緒獲取同步元件成功直接返回,而false的話AQS會將當前獻策還給你插入同步佇列等一系列的方法。

總的來說,同步元件通過重寫AQS的方法實現自己想要表達的同步語義,而AQS只需要同步元件表達true和false即可,AQS會針對true和false不同的情況做不同的處理。

 

相關文章