多線程設計模式:第二篇 - 四種基礎模式

一,單線程模式

         單線程模式是指在臨界區域內,同一時間內只允許一個線程執行處理。

         下面的代碼示例使三個人頻繁的通過一道門,當經過門的時候記錄通行者的姓名和出生地,同時增加已通過門的人數。

/**
 * @author koma <[email protected]>
 * @date 2018-10-15
 */
public class Main {
    public static void main(String[] args) {
        Gate gate = new Gate();
        new UserThread(gate, "Alice", "Alas").start();
        new UserThread(gate, "Bobby", "Brazli").start();
        new UserThread(gate, "Chirs", "Canda").start();
    }
}

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();
    }

    public String toString() {
        return "No."+counter+": "+name+", "+address;
    }

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

public class UserThread extends Thread {
    private final Gate gate;
    private final String myname;
    private final String myaddress;

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

    @Override
    public void run() {
        System.out.println(myname+" BEGIN");
        while (true) {
            gate.pass(myname, myaddress);
        }
    }
}

         運行上面的示例程序會發現,程序執行混亂,主要原因是因爲 Gate 類作爲共享資源,在多線程環境下是非線程安全的,pass() 和 toString() 方法作爲程序的臨界區,在多線程環境下時可以被多個線程同時調用執行,屬於非線程安全方法。

         對於非線程安全的方法,在同時被多個線程調用執行時,實例的狀態就會發生混亂,這時就應該保護該方法,使其不能夠被多個線程同時調用,這時就需要用到單線程模式,即同一時刻只能允許一個線程調用執行。在 Java 中,通過給非線程安全方法加上 synchronized 聲明來進行保護。修改後代碼如下:

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

public synchronized String toString() {
    return "No."+counter+": "+name+", "+address;
}

         對於 check() 方法不需要保護的原因是,首先 check() 是一個私有方法,不能夠被外部實例隨意調用,其次調用 check() 方法的 pass() 方法已經被保護起來,因此 check() 方法就無需再保護,但是由於 synchronized 的鎖是可重入的,因此即使給 check() 方法加上 synchronized 聲明,也不影響程序運行結果。

1,生存性和死鎖

         多線程程序評價標準中最重要的是線程安全性和生存性。單線程模式保證了線程的安全性,但是當使用單線程模式時,如果稍不注意則會使程序發生死鎖,從而讓程序失去生存性。

         在單線程模式中,當滿足下列條件時,就會發生死鎖:

  • 存在多個共享資源
  • 線程在持有某個資源的鎖時,還需要獲取其它資源的鎖
  • 獲取共享資源的鎖的順序不固定

2,性能

         使用單線程模式會顯著降低程序性能,主要原因是在進入 synchronized 方法時需要先獲取實例的鎖,而獲取鎖是需要時間的,如果需要獲取多個資源的鎖,則耗費的時間會更長。獲取鎖時,如果有其它線程正在持有鎖,那麼會產生線程衝突,當發生線程衝突時,程序的整體性能會隨着線程等待時間的增加而下降。

3,synchronized 與 Before/After 模式

         不管是 synchronized 方法還是 synchronized 代碼塊,都可以看作是在 "{" 處獲取鎖,在 "}" 處釋放鎖。這與顯式的獲取鎖,釋放鎖的程序存在很大的區別,如下:

method() {
    lock();
    //do method
    unlock();
}

         這種顯式的方式會根據 //do method 部分或者執行 return,或者拋出異常而導致鎖無法被釋放,而 synchronized 則會保證鎖一定會被釋放。要想讓顯式的鎖操作達到和 synchronized 同樣的效果,則需要使用到一種簡單的 Before/After 模式實現,如下:

method() {
    lock();
    try{
        //do method
    } finally {
        unlock();
    }
}

         由於 Java 規範保證了 finally 部分一定會被執行,從而可以保證鎖一定會被釋放。

4,synchronized 與 原子操作

         synchronized 方法只允許一個線程同時執行,從線程的角度來看,這個方法的執行是不可被分割的,這種不可分割的操作,通常稱爲原子(Atomic)操作。

         Java 規範定義了一些原子操作,如,char,int 等基本類型的操作,對象引用類型的賦值和引用等。因此對於這類操作,無需加上 synchronized 關鍵字。但是對於 long 和 double 類型的引用賦值則不是原子操作,因此就必須使用單線程模式,最簡單的方法就是在 synchronized 方法中執行操作。另外一種方法是在該類字段聲明上加上 volatile 關鍵字,加上 volatile 關鍵字之後,對該字段的操作就是原子的了。

         一般情況下對於 long 和 double 操作,Java虛擬機也將其視爲原子操作,但是這僅僅是虛擬機的實現,對於有些虛擬機可能並沒有實現,因此對於 long 和 double 操作,在多線程環境下,通常建議加上 volatile 關鍵字聲明或者使用 java 提供的 atomic 包。

5,計數信號量

         單線程模式用於確保臨界資源同一時刻只能由一個線程使用,那麼如果想實現在同一時刻只能有 N 個線程使用時,這時就可以考慮使用計數信號量來實現。

         Java juc包中提供了用於表示計數信號量的 Semaphore 類。該類中 acquire() 方法用於確保存在可用資源,當存在可用資源時,線程會立即返回,同時內部信號量減1,當無可用資源時,線程則阻塞,直到有可用資源出現。release() 方法用於釋放資源,釋放資源後,內部信號量加1,同時會喚醒一個等待在 acquire() 方法上的線程。

         下面的示例程序演示了10個線程去爭搶3個資源的情形,在同一時刻只能有3個線程使用共享資源,其它線程則需要等待,代碼如下:

/**
 * @author koma <[email protected]>
 * @date 2018-10-15
 */
public class TestCounter {
    public static void main(String[] args) {
        new TestCounter().run();
    }

    public void run() {
        //創建三個資源,同一個時刻只能有三個線程同時使用
        BoundedResource resource = new BoundedResource(3);

        //創建10個線程去爭搶資源
        for (int i = 0; i < 10; i++) {
            new UseThread(resource).start();
        }
    }

    class UseThread extends Thread {
        private final BoundedResource resource;
        private final Random random = new Random();

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

        @Override
        public void run() {
            while (true) {
                try {
                    resource.use();
                    Thread.sleep(random.nextInt(3000));
                } catch (InterruptedException e) {
                }
            }
        }
    }

    class BoundedResource {
        private final Semaphore semaphore;
        private final int permits;
        private final Random random = new Random();

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

        public void use() throws InterruptedException {
            semaphore.acquire(); //申請資源
            try {
                System.out.println("BEGIN: used = "+(permits-semaphore.availablePermits()));
                Thread.sleep(random.nextInt(500));
                System.out.println("END: used = "+(permits-semaphore.availablePermits()));
            } finally {
                semaphore.release(); //使用 Before/After 模式保證資源一定會被釋放
            }
        }
    }
}

二,不可變模式

        不可變模式是指在該模式中存在可以確保類實例的狀態一定不發生變化的類,多線程環境下在訪問這些不可變的實例的時候,不需要執行耗時的互斥處理,從而提高程序的性能。如下代碼示例,Person 類即是一個遵循不可變模式的不可變類。

/**
 * @author koma <[email protected]>
 * @date 2018-10-15
 */
public final class Person {
    private final String name;
    private final String address;

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return this.name;
    }

    public String getAddress() {
        return this.address;
    }

    public String toString() {
        return "[Person: name = "+name+", address = "+address+"]";
    }
}

        在該類中,類被聲明爲 final 類型,從而確保該類沒有子類,類成員被聲明爲 private 確保類成員不能在類外部被修改,而類方法中只有 getter 方法,也確保了通過該類也不能夠修改類成員內容,而類成員同樣也聲明爲 final 且在構造方法中賦值,則類成員內容在類實例化之後即不可能再被修改。以上種種措施,都是爲了保證 Person 類的不可變性。那麼在多線程環境下使用該類時,就可以省去多線程互斥處理,從而提供程序性能。

1,不可變模式的應用場景

  • 實例被創建後,狀態將不再發生變化
            實例的狀態是由字段的值決定的,因此將字段聲明爲 final 且不存在 setter 方法是必要措施,但是這還不夠充分,因爲即使字段的值不變,字段所引用的實例也有可能發生變化。
  • 實例是共享的,且被頻繁訪問
            不可變模式的有點是不再需要 synchronized 保護,這就意味着能夠在不失去安全性和生存性的前提下提高程序性能。

2,可變類和不可變類

        不可變類的使用場景比較微妙,因爲大部分的類可能都需要使用 setter 方法,這時我們可以重新審視該類,看是否能夠把類拆分成一個可變類和一個不可變類,然後再設計成通過可變類可以創建不可變類,反過來通過不可變類也可以創建可變類,這樣在不可變類中就可以應用不可變模式了。Java 中使用這種設計方法的經典示例就是 String 類和 StringBuffer 類。

3,集合類和多線程

        Java中提供了常見的集合操作類,這些類大部分都是非線程安全的,因此,在多線程環境下使用集合類時一定要確定集合類的線程安全性。

三,守護-等待模式

        守護等待模式是通過讓線程等待來保證實例的安全性,即如果現在執行處理會造成問題,那麼就讓線程進行等待。

        下面的示例代碼實現了一個簡單的線程間通信,Client 線程會將請求的實例傳遞給 Server 線程,當 Server 線程試圖獲取請求實例時,如果當前還沒有可以的請求實例,那麼 Server 線程會進行等待,如下:

/**
 * @author koma <[email protected]>
 * @date 2018-10-15
 */
public class Main {
    public static void main(String[] args) {
        RequestQueue queue = new RequestQueue();
        new ClientThread(queue, "Alice", 3141592L).start();
        new ServerThread(queue, "Bobby", 6535897L).start();
    }
}

public class Request {
    private final String name;

    public Request(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    @Override
    public String toString() {
        return "[ Request "+name+" ]";
    }
}

public class RequestQueue {
    private final Queue<Request> queue = new LinkedList<>();

    public synchronized Request getRequest() {
        while (queue.peek() == null) { //當沒有可用的請求實例時等待
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        return queue.remove();
    }

    public synchronized void putRequest(Request request) {
        queue.offer(request);
        notifyAll(); //喚醒等待的線程
    }
}

public class ServerThread extends Thread {
    private final Random random;
    private final RequestQueue queue;

    public ServerThread(RequestQueue queue, String name, Long seed) {
        super(name);
        this.random = new Random(seed);
        this.queue = queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            Request request = queue.getRequest();
            System.out.println(Thread.currentThread().getName()+" handles "+request);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
            }
        }
    }
}

public class ServerThread extends Thread {
    private final Random random;
    private final RequestQueue queue;

    public ServerThread(RequestQueue queue, String name, Long seed) {
        super(name);
        this.random = new Random(seed);
        this.queue = queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            Request request = queue.getRequest();
            System.out.println(Thread.currentThread().getName()+" handles "+request);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
            }
        }
    }
}

        通過上述代碼可以知道,守護-等待模式主要利用了線程的通知-等待機制。守護-等待模式中的主要角色是一個持有被守護方法的類,進入到該方法中的線程是否要等待取決於守護條件。

        上述示例代碼使用 LinkedList 類實現了 RequestQueue 類,實際上 Java 在 juc 包中提供了與該類功能類似的一個類,那就是 LinkedBlockingQueue,由於該類內部已經實現了 wait() 和 notify() 機制,因此使用該類可以簡化 RequestQueue 的實現,如下:

public class RequestQueue {
    private final BlockingQueue<Request> queue = new LinkedBlockingQueue<>();

    public Request getRequest() {
        Request request = null;
        try {
            request = queue.take(); //取出隊首元素,爲空時 wait()
        } catch (InterruptedException e) {
        }
        return request;
    }

    public void putRequest(Request request) {
        try {
            queue.put(request); //向隊尾添加元素,並喚醒等待的線程
        } catch (InterruptedException e) {
        }
    }
}

四,停止-返回模式

        停止-返回模式是說,如果現在不適合執行這個操作,那麼就直接返回。

        停止-返回模式的重點是當操作的守護條件不允許執行時直接返回,而非等待。例如下面的示例代碼,ChangerThread 會不定期的修改 Data,同時保存,同時後臺也會運行一個 SaverThread,該線程會定時檢查 Data 的修改是否保存,如果已經保存則不做任何操作,直接返回,如果還未保存,則執行保存。這有點兒類似於我們常見的文檔自動保存功能,如下:

/**
 * @author koma <[email protected]>
 * @date 2018-10-15
 */
public class Main {
    public static void main(String[] args) {
        Data data = new Data("data.txt", "(empty)");
        new ChangerThread("ChangerThread", data).start();
        new SaverThread("SaverThread", data).start();
    }
}

public class Data {
    private final String filename;
    private String content;
    private boolean changed;

    public Data(String filename, String content) {
        this.filename = filename;
        this.content = content;
        this.changed = true;
    }

    public synchronized void change(String newContent) {
        content = newContent;
        changed = true;
    }

    public synchronized void save() throws IOException {
        if (!changed) {
            return;
        }

        doSave();
        changed = false;
    }

    private void doSave() throws IOException {
        System.out.println(Thread.currentThread().getName()+" calls doSave, content = "+content);
        Writer writer = new FileWriter(filename);
        writer.write(content);
        writer.close();
    }
}

public class ChangerThread extends Thread {
    private final Data data;
    private final Random random = new Random();

    public ChangerThread(String name, Data data) {
        super(name);
        this.data = data;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; true; i++) {
                data.change("No."+i);
                Thread.sleep(random.nextInt(1000));
                data.save();
            }
        } catch (InterruptedException e) {
        } catch (IOException e) {
        }
    }
}

public class SaverThread extends Thread {
    private final Data data;

    public SaverThread(String name, Data data) {
        super(name);
        this.data = data;
    }

    @Override
    public void run() {
        try {
            while (true) {
                data.save();
                Thread.sleep(1000);
            }
        } catch (IOException e) {
        } catch (InterruptedException e) {
        }
    }
}

        示例代碼的重點是在 save() 方法中,當我們發現 changed 爲 true 即數據修改已經保存之後,線程立即返回而不是等待,這就是停止-返回模式和守護-等待模式的不同,也是其特點所在。基於該特點,那麼停止-等待模式的應用場景可以舉例如下:

  • 並不需要執行時,就像示例程序一樣
  • 當守護條件第一次成立時,例如下面這個在多線程環境下永遠都只初始化一次的類。

    public class InitTest {
    private boolean inited = false;
    
    public synchronized void init() {
        if (inited) { //當類已經被初始化過時,不做任何操作,這裏不能使用 wait()
            return;
        }
        //do init
        inited = true;
    }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章