Java 多線程設計模式之 Single Threades Execution

Single Threades Execution 模式

所謂 Single Threades Execution 模式,意即「以一個線程執行」。就像獨木橋同一時間內只容許一我的通行同樣,該模式用於設置限制,以確保同一時間內只能讓一個線程執行處理。java

Demo

不使用 Single Threades Execution 模式的程序

使用程序模擬三我的頻繁地經過一個只容許一我的通過的門情形。當人們經過門的時候,統計人數便會遞增。另外程序還會記錄通行者的「姓名和出生地」編程

類一覽表

名字 說明
Main 建立門,並讓三我的不斷地經過的類
Gate 表示門的類。它會在人們經過門時記錄其姓名與出生地
UserThread 表示人的類。人們不斷地經過門
// Main.java

public class Main {

    public static void main(String[] args) {
        Gate gate = new Gate();
        new UserThread(gate, "Bob", "Britain").start();
        new UserThread(gate, "Cao", "China").start();
        new UserThread(gate, "Uber", "USA").start();
    }
}
複製代碼
// Gate.java

public class Gate {

    private int counter = 0;
    private String name = "Nobody";
    private String address = "NoWhere";

    public void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    private void check() {
        if (this.name.charAt(0) != this.address.charAt(0)) {
            System.out.println("******** BROKEN ********** : " + toString());
        }
    }

    @Override
    public String toString() {
        return "No. " + this.counter + " : " + this.name + " , " + this.address;
    }
}
複製代碼
// UserThread.java

public class UserThread extends Thread {

    private final Gate gate;
    private final String name;
    private final String address;

    public UserThread(Gate gate, String name, String address) {
        this.gate = gate;
        this.name = name;
        this.address = address;
    }

    @Override
    public void run() {
        System.out.println(this.name + " BEGIN");
        while (true) {
            gate.pass(this.name, this.address);
        }
    }
}
複製代碼

當這個程序執行時,時間點不一樣,生成的結果也會不同,如下是打印出來的 log設計模式

Bob BEGIN
Cao BEGIN
******** BROKEN ********** : No. 59622 : Bob , Britain
Uber BEGIN
******** BROKEN ********** : No. 77170 : Uber , USA
******** BROKEN ********** : No. 89771 : Uber , USA
******** BROKEN ********** : No. 93128 : Cao , China
******** BROKEN ********** : No. 95654 : Uber , USA
******** BROKEN ********** : No. 98440 : Cao , China
******** BROKEN ********** : No. 102283 : Cao , China
******** BROKEN ********** : No. 104491 : Cao , China
******** BROKEN ********** : No. 106791 : Uber , USA
******** BROKEN ********** : No. 110022 : Uber , USA
******** BROKEN ********** : No. 112073 : Uber , USA
******** BROKEN ********** : No. 113973 : Uber , USA
******** BROKEN ********** : No. 77170 : Uber , USA
******** BROKEN ********** : No. 116050 : Bob , China
******** BROKEN ********** : No. 117334 : Bob , Britain
******** BROKEN ********** : No. 119992 : Bob , USA
******** BROKEN ********** : No. 124427 : Uber , USA
******** BROKEN ********** : No. 117152 : Bob , Britain
******** BROKEN ********** : No. 129298 : Bob , China
******** BROKEN ********** : No. 130552 : Cao , Britain
******** BROKEN ********** : No. 147176 : Cao , China
******** BROKEN ********** : No. 148546 : Uber , USA
複製代碼

經過 log 能夠知道運行結果與預期不一致,因此說 Gate 類是不安全的,是非線程安全類。安全

若是仔細看一下 counter 的值,最開始顯示 BROKEN 的時候,counter 的值已經變爲了 59622。也就是說,在檢察處第一個錯誤的時候 Gate 的 pass 方法已經運行了 5 萬屢次了。在這裏,由於 UserThread 類的 run 方法執行的是無限循環,因此才檢查除了錯誤。可是若是隻測試幾回,是根本找不出錯誤的。多線程

這就是多線程程序設計的難點之一。若是檢察出錯誤,那麼說明程序並不安全。可是就算沒有檢察出錯誤,也不能說程序就必定是安全的。dom

調試信息也不可靠

仔細看 log 會發現還有一個奇怪的現象,好比:ide

******** BROKEN ********** : No. 59622 : Bob , Britain
複製代碼

雖然此處輸出了 BROKEN 信息,可是姓名和出生地首字母是同樣的。儘管顯示了 BROKEN,可是調試信息好像並無錯。函數

致使這種現象的緣由是,在某個線程執行 check 方法時,其餘線程不斷執行 pass 方法,改謝了 name 字段和 address 字段的值。測試

這也是多線程程序設計的難點之一。若是顯示調試信息的代碼自己就是非線程安全的,那麼顯示的調試信息就極可能是錯誤的。ui

若是連操做測試和調試信息都沒法確保安全性,那就進行代碼評審吧。多我的一塊兒仔細閱讀代碼,確認是否會發生問題,這是確保程序安全性的一個有效方法。

修改 Gate 類使其線程安全

// Gate.java

public class Gate {
    ...
    
    public synchronized void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }
    
    ...
}
複製代碼

以後程序就能夠正常的運行,也不在打印 BROKEN 的 log 信息了

Single Threaded Execution 模式概括

SharedResource 共享資源

在剛纔的示例中,Gate 類扮演 SharedResource 的角色

SharedResource 角色是可被多個線程訪問的類,包含不少方法,但這些方法主要分爲以下兩類:

  • safeMethod: 多個線程同時調用也不會發生問題的方法
  • unsafeMethod:多個線程同時調用會發生問題,所以必須加以保護的方法

而 unsafeMethod 在被多個線程同時執行時,實例狀態有可能發生分歧。這時就須要保護該方法,使其不被多個線程同時訪問。 Java 則是經過將 unsafeMethod 聲明爲 synchronized 方法來進行保護

死鎖

在該模式下,知足下列條件時,死鎖就會發生

  • 存在多個 SharedResource 角色
  • 線程在持有着某個 SharedResource 角色鎖的同時,還想獲取其餘 SharedResource 角色的鎖
  • 獲取 SharedResource 角色的鎖的順序並不固定

原子操做

不可分割的操做一般稱爲原子操做。

上述示例中 Gate類是線程安全的 咱們將 pass 聲明爲了 synchronized 方法,這樣 pass 方法也就成爲了原子操做

Java 編程規範中定義了一些原子操做。例如 char、int 等基本類型的賦值和引用操做都是原子的。另外,對象等引用類型的賦值和引用操做也是原子的。因爲自己就是原子的,因此就算不加上 synchronized,這些操做也不會被分割。可是 long、double 的賦值和引用操做並非原子的

總結以下:

  • 基本類型、引用類型的賦值和引用是原子操做
  • 但 long 和 double 的賦值和引用是非原子操做
  • long 或 double 在線程間共享時,須要將其放入 synchronized 中操做,或者聲明爲 volatile

計數信號量和 Semaphore 類

上面介紹 Single Threaded Execution 模式用於確保某個區域「只能由一個線程」執行。下面咱們將這種模式進一步擴展,以確保某個區域「最多隻能由 N 個線程」執行。這時就要用計數信號量來控制線程數量。

java.util.concurrent 包提供了表示計數信號量的 Semaphore 類

資源的許可個數將經過 Semaphore 的構造函數來指定

Semaphore 的 acquire 方法用於確保存在可用資源。當存在可用資源時,線程會當即從 acquire 方法返回,同時信號量內部的資源個數會減 1 。 如無可用資源,線程阻塞在 acquire 方法內,直至出現可用資源。

Semaphore 的 release 方法用於釋放資源。釋放資源後,信號量內部的資源個數會增長 1。另外若是 acquire 中存在等待的線程,那麼其中一個線程會被喚醒,並從 acquire 方法返回。

示例

// BoundedResource.java

public class BoundedResource {

    private final int permits;
    private final Semaphore semaphore;
    private final Random random = new Random(314159);

    public BoundedResource(int permits) {
        this.semaphore = new Semaphore(permits);
        this.permits = permits;
    }

    public void use() throws InterruptedException {
        try {
            this.semaphore.acquire();
            doUse();
        } finally {
            this.semaphore.release();
        }

    }

    private void doUse() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " : BEGIN used = " + (this.permits - this.semaphore.availablePermits()));
        Thread.sleep(this.random.nextInt(500));
        System.out.println(Thread.currentThread().getName() + " : END used = " + (this.permits - this.semaphore.availablePermits()));
    }
}
複製代碼
// SemaphoreThread.java

public class SemaphoreThread extends Thread{

    private final Random random = new Random(26535);
    private final BoundedResource resource;

    public SemaphoreThread(BoundedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        try {
            while (true) {
                this.resource.use();
                Thread.sleep(this.random.nextInt(2000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼
// Main.java

public class Main {

    public static void main(String[] args) {
        BoundedResource boundedResource = new BoundedResource(3);
        new SemaphoreThread(boundedResource).start();
        new SemaphoreThread(boundedResource).start();
        new SemaphoreThread(boundedResource).start();
    }
}
複製代碼

打印結果:

Thread-0 : BEGIN used = 2
Thread-2 : BEGIN used = 3
Thread-1 : BEGIN used = 2
Thread-2 : END used = 3
Thread-1 : END used = 2
Thread-0 : END used = 1
Thread-2 : BEGIN used = 1
Thread-2 : END used = 1
Thread-1 : BEGIN used = 1
Thread-0 : BEGIN used = 2
Thread-1 : END used = 2
Thread-0 : END used = 1
Thread-2 : BEGIN used = 1
Thread-2 : END used = 1
Thread-1 : BEGIN used = 1
Thread-0 : BEGIN used = 2
Thread-2 : BEGIN used = 3
Thread-0 : END used = 3

複製代碼

參考

相關文章
相關標籤/搜索