1. 程式人生 > >Java併發程式設計實戰————Semaphore訊號量的使用淺析

Java併發程式設計實戰————Semaphore訊號量的使用淺析

引言

本篇部落格講解《Java併發程式設計實戰》中的同步工具類:訊號量 的使用和理解。

從概念、含義入手,突出重點,配以程式碼例項及講解,並以生活中的案例做類比加強記憶。

什麼是訊號量

Java中的同步工具類訊號量即計數訊號量(Counting Semaphore),是用來控制訪問某個特定資源的運算元量,或同時執行某個指定操作的數量。可以簡單理解為訊號量用來限制對某個資源的某種操作的數量。

一般用於實現某種資源池,或對容器施加邊界。

 訊號量管理著一組有限個數的虛擬許可(permit),而許可的數量就是限制特定運算元量的關鍵。

訊號量的使用

前面已經說過,訊號量一般用於實現某種資源池或對容器施加邊界,這都是一個對特定操作的限制用途。那麼想象一下,如何限制操作的數量,達到為一個再普通不過的容器施加邊界的效果呢?答案是給容器的某種操作(可以是新增或刪除元素,應該廣義的理解“某種操作”這個關鍵字眼)增加一道執行許可,只有在獲得許可的情況下才可以執行這個操作:

上圖左邊是普通的對容器的操作,右邊是有了訊號量的對容器的操作。可以看出,在增加了中間的訊號量之後,對容器的操作將會受限。

Semaphore

瞭解了訊號量的大概含義,那麼進一步深入到Java類庫的層面,JDK為開發者提供了java.util.concurrent包下的Semaphore類,它的含義就是上面所述的訊號量,管理著一組permit。

以“為容器施加邊界”這一訊號量用途為例。首先我們要明確一點,使用訊號量的方式來實現施加邊界的方式,其針對的是操作而不是容器的容量!再一次重申,是限制了操作,而不是容器的容量!

強調限制操作,是為了要明白一點:使用訊號量來施加邊界,必然會對這個容器的某些操作進一步封裝。比如新增方法,就會在呼叫add之前先行呼叫Semaphore物件的acquire()方法,在與這個操作相反的操作中去release()。並且,acquire()方法是阻塞式的,這就代表沒有閒置許可的時候,操作將會阻塞直到有許可被釋放。

下面程式碼用訊號量來對HashSet這個最普通的容器來施加一個新增限制,進一步封裝

,使其成為一個有界的阻塞式的容器

public class BoundedHashSet<T> {
    private final Set<T> set;
    private final Semaphore sem;

    public BoundedHashSet(int bound) {
        this.set = Collections.synchronizedSet(new HashSet<>());
        this.sem = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        } finally {
            if (!wasAdded)
                sem.release();
        }
    }
    
    public boolean remove(Object o) {
        boolean wasRemoved = set.remove(o);
        if (wasRemoved)
            sem.release();
        return wasRemoved;
    }
    /** 只是為了方便列印的 */
    public void print() {
        System.out.print(Thread.currentThread().getName() + " : ");
        this.set.forEach(o -> System.out.print(o + " "));
    }
    /** 用於測試的主方法 */
    public static void main(String[] args) {
        BoundedHashSet<String> names = new BoundedHashSet<>(5);

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    names.add("name" + i);
                    
                    names.print();
                    System.out.println();
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "TH-ADD").start();
        
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "--------執行清理,刪除name" + i);
                names.remove("name" + i);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"TH-REMOVE").start();
    }
}

執行結果如下:

我們用一個執行緒為這個“有界”容器每隔1秒鐘新增一個元素,然後另一個執行緒每隔10秒鐘移除一個元素。且初始化了這個容器的訊號量為5,那麼當容器中新增元素的數量達到5之後,5個許可全部被佔用,新增操作將進入阻塞狀態,直到remove的時候釋放一個許可,才可以繼續新增元素。從上述結果可以看出兩點:

1、擁有5個許可的訊號量成功的限制了容器的元素個數(即為容器施加了一個邊界);

2、新增的操作在沒有獲得許可的情況下將進入阻塞狀態,在執行的過程中也恰恰印證了這一點:當remove執行並release()之後,新增操作會立刻執行。

生活中的類比

其實這個類比博主認為,從嚴謹的角度來講,並不是完全符合訊號量的概念,但是我們可以類比的同時找出不同點,不僅有效的通過生活案例理解了訊號量,還對與之不同的地方增加了深刻的印象,所以還是決定拿出來供大家參考。

上過學的同學可能都知道,學校有獎學金制度。雖然我沒怎麼得過獎學金,但是大概的邏輯還是比較好理解。

學校的獎學金制度是怎樣的呢?

學校每年都會給全校的學生指定數量的全額獎學金名額,比如全額獎學金5名。那麼如果想獲得全額獎學金,就必須先獲得名額才行。

從這個簡單的邏輯我們可以找出關鍵的與訊號量中的概念相匹配的內容:

獎學金 = 特定資源

獲得(獎學金) = 指定操作(如remove操作)

名額 = 一組定額許可的訊號量

名額已滿,來年再報 = 操作阻塞,等待釋放許可

 有了上面的等式,訊號量的神祕面紗就算徹底被我們揭開了,原來它就是一個管理一組定額許可的通行證,要想執行操作,那就必須先得到許可,否則就阻塞。

總結

訊號量的概念:限制運算元量。

一個類:Semaphore ,兩個方法:acquire()、release()。

用途:對容器施加邊界,對容器的操作的再封裝。

另外,獎學金和訊號量之間的類比並不完全匹配,不過這種程度的類比已經相當清晰,至於哪些資訊有所差異,留給各位看官自己去挖掘。如果有什麼新的發現,真誠希望在部落格下方留言。

願所有熱愛程式設計的開發者共同進步!