多線程設計模式——Read-Write Lock模式和Future模式分析

本文內全部實現的代碼均附在文末,有須要能夠參考。(好奇寶寶們能夠粘貼下來跑一下java

多線程程序評價標準

  • 安全性:編程

    ​ 安全性就是不損壞對象。也就是保證對象內部的字段的值與預期相同。設計模式

  • 生存性:安全

    ​ 生存性是指不管何時,必要的處理都必定可以執行。失去生存性最典型的例子就是「死鎖」。網絡

  • 可複用性:多線程

    ​ 指類可以重複利用。若類可以做爲組件從正常運行的軟件裏分割出來,說明這個類有很高的複用性。框架

  • 性能:dom

    ​ 指可以快速、大批量地執行處理。主要影響因素有:吞吐量、響應性、容量等。異步

這裏還要再區分一下這四條。前兩條是程序正常運行的必要條件;後兩條是程序提升質量的必要條件。性能

任何模式都有一個相同的「中心思想」

安全性和生存性是基礎,是全部模式都必須保證的;可複用性和性能是目的,是全部模式誕生的意義。」

上面這句話是咱們每個使用設計模式的人,甚至是本身編寫代碼的人所應該牢記在心的。

在接下來分析的兩個模式中,我會用實際的設計模式的例子來幫助你們理解上面這句話的含義。

Read-Write Lock 模式

RW-Lock模式特色

  • 在執行讀取操做以前,線程必須獲取用於讀取的鎖。
  • 在執行寫入操做以前,線程必須獲取用於寫入的鎖。
  • 多個線程能夠同時讀取,可是讀取時,不能夠寫入。
  • 至多有一個線程正在寫入,此時其餘線程不能夠讀取或寫入。

通常來講,執行互斥處理(也是必要的)會下降程序性能(這裏的互斥處理指使用synchronized關鍵字)。可是經過這個模式,將針對寫入的互斥處理和讀取的互斥處理分開考慮,則能夠提升程序性能。(具體性能提高效果請見下文「性能對比」一節)

衝突總結

多線程讀寫時總共有4種狀況,會發生衝突的有三種。下面給出衝突表格:

讀取 寫入
讀取 無衝突 讀和寫的衝突 RW Conflict
寫入 讀和寫的衝突 RW Conflict 寫和寫的衝突 WW Conflict

手搓RW Lock模式代碼

這部份內容是爲了幫助你們更好地理解RW Lock的實現原理和過程,實際操做中咱們沒必要編寫這麼多代碼來實現讀寫鎖。可是在這裏強烈建議認真閱讀此部分,瞭解原理後對使用JAVA自帶包或是實現特殊需求都會大有裨益!

類圖

Data類中的buffer字段是讀寫的信息。ReaderThread類是讀取的線程,WriterThread是寫入的線程。Data類中還保有一個ReadWriteLock類的實例,它是這個模式的主角,起到保護讀寫的做用。

Data類


第一行紅線處,lock是一個ReadWriteLock類的實例,起到保護讀寫的做用。

第2、三行紅線處,分別是readLock方法和readUnlock方法,夾在中間的是doRead方法(進行讀取的方法)。

第4、五行紅線處,分別是writeLock方法和writeUnlock方法,夾在中間的是doWrite方法(進行寫入的方法)。

Data類中還有用於模擬耗時的方法,即假定寫入操做耗時比讀取長(符合一般程序的狀況)。

這裏提到的」夾在中間「的說法,實際上是另外一種設計模式——「Before/After模式」。因爲它的使用有一些坑點,我這裏先「中斷」一下,簡單講一下「Before/After模式」。

P.S. Before/After模式

前置處理(此模式中爲獲取鎖)
    try{
        實際的操做(有return也會執行finally語句塊中的內容)
    } finally {
        後置處理(此模式中爲釋放鎖)
    }

以上代碼爲Before/After模式的基本框架。

此模式使用有兩點要特別注意!!!

  • try語句後面必定要跟着finally語句塊!finally語句塊的含義是:只要進入了try語句塊,就必定會在最後執行一次finally語句塊內的代碼,即便try語句塊內有return語句也會執行。在這個模式中,使用finally語句就保證了,獲取的鎖在最後必定會被釋放掉,避免"死鎖"發生。

  • 前置處理的語句必定要放在try語句塊外面!這一點可能會有不少人不理解,放在裏面仍是外面有什麼區別?回答是:在絕大多數狀況下,確實沒有區別。 可是當線程被interrupt時,程序就有可能出現過多調用readUnlock和writeUnlock方法的風險。假如如今程序正在lock.readLock()中進行wait,此時該線程被interrupt,那麼程序會拋出InterruptedException異常,並退出readLock方法。這時readingReaders字段並不會遞增。

    從readLock方法退出的線程回跳到finally語句塊,執行lock.readUnlock()。在這個方法中,,以前未遞增的readingReaders字段會執行遞減操做,該字段的值會與咱們預期不一樣(變得比正常要小)。這就頗有可能引起難以察覺的bug。

    (上面兩段中出現的方法名和字段不知道不要緊,它們都在下面即將介紹的ReadWriteLock類中,建議你們看完下面的ReadWriteLock類的介紹再回來理解一下這部分,很重要!!很容易出bug!!!

ReadWriteLock類

該類中保存有四個私有字段,前三個字段的含義很好理解,見圖片中的代碼註釋。

在這裏,特別強調preferWriter字段!這是保證程序運行結果達到預期的重要一環,其含義和用法須要你們好好理解。這個preferWriter表明的含義是讀取和寫入二者之間的優先級關係。當preferWriter字段爲true時,表明寫入優先;爲false時,表明讀取優先。那麼這個讀取或寫入的優先又是如何經過這一個布爾值實現的呢?這裏就體現出了ReadWriteLock類的設計巧妙之處。

咱們看readLock方法中的守護模式(while+wait)的守護條件(while成立的條件)。(見上圖中第二行紅線)這行代碼的含義是若是有正在寫入的線程(數據正在被寫入)或是寫入優先而且有正在等待寫入的線程,那麼讀取的線程就要wait。這裏,preferWriter字段發揮了它關鍵的做用。

再看readUnlock方法中對preferWriter字段的操做(第三行紅線)。這裏的含義是,在讀取鎖釋放時,就把preferWriter字段置爲true。由於讀取鎖釋放時,必定表示已經進行完一次讀取操做了,此時應該把優先權讓給寫入操做,因此將preferWriter置爲true。

同理,writeUnlock方法中對preferWriter字段的操做(第四行紅線)也即表明進行完一次寫入操做後,要把優先權交給讀取操做,即把preferWriter字段置爲false。

這就像兩我的卻只有一個水瓶,一我的喝完一口水以後就要把水瓶交給對方,否則就會出現渴死的現象。

那麼若是把ReadWriteLock類中的preferWriter字段去掉,程序運行起來會是什麼樣子呢?以下:

讀取線程比寫入線程多,並且讀取操做耗時短,因此讀取線程會一直搶佔鎖,致使寫入線程沒法寫入。這就是程序「渴死」的樣子了。(你們有興趣能夠把文末代碼粘貼下來,把preferWriter字段去掉本身跑一下

正確運行結果

正確的運行結果應該是讀取一段時間就寫入一次,這樣不斷循環。因此讀取的內容應該不斷變化。結果見下圖:

適用場合

  • 讀取操做繁重時

    ​ 即read操做很耗費時間。這種狀況下,使用這種模式比Single Thread Execution模式(使用synchrnized關鍵字)更適合。反之,Single Thread Execution模式性能更好。

  • 讀取頻率比寫入頻率高時

    ​ 該模式的優勢在於Reader角色之間不會發生衝突,這樣能夠避免阻塞而耗費時間。但若寫入頻率很高,則Writer角色會頻繁打斷Reader角色的讀取工做,致使性能提高不會很明顯。

「邏輯鎖」vs「物理鎖」

你們確定都很熟悉經過synchronized關鍵字來進行線程同步控制,由於synchronized關鍵字能夠獲取實例的鎖。可是這裏synchronized關鍵字所獲取的鎖是JVM爲每個實例提供的一個物理鎖。每一個實例只有一個物理鎖,不管如何編寫程序,也沒法改變這個物理鎖的運行。

咱們這個Read Write Lock模式中所提供的「用於寫入的鎖」和「用於讀取的鎖」都是邏輯鎖。這個鎖不是JVM所規定的結構,而是編程人員本身實現的一種邏輯結構。這就是所謂的邏輯鎖。咱們能夠經過控制ReadWriteLock類來控制邏輯鎖的運行。

那麼這兩者的關係是什麼呢?其實,ReadWriteLock類提供的兩個邏輯鎖的實現,都是依靠ReadWriteLock實例持有的物理鎖完成的。

而此處咱們也來解釋一下上節中所說的,讀取不繁重時,使用咱們本身所構建的邏輯鎖就會致使比使用synchronized關鍵字(物理鎖)多不少邏輯操做,這樣多出來的邏輯操做所耗費的時間也許會大於線程被阻塞的時間。這樣就會致使本模式反而會比Single Thread Execution性能差。

性能對比

示例代碼中一共有6個讀取線程,兩個寫入線程。在本節性能對比中,我讓每一個讀取線程進行20次讀取後就輸出運行時間而後終止。如下兩張圖分別爲使用Read-Write Lock模式耗時和使用synchronized關鍵字耗時。

Read-Write Lock模式:

synchronized關鍵字:

從以上兩圖輸出的時間能夠看出,在每一個線程讀取20次的狀況下,使用Read-Write Lock模式能夠比synchronized關鍵字節省三分之二(7秒鐘左右)的時間。這在大量讀取的程序中,會給程序性能帶來極大的提高!!!(固然對於OO第二單元電梯做業來講,因爲讀寫頻率差別不大並且讀取並不繁瑣,因此在電梯程序中使用Read-Write Lock模式性能提高並不明顯。不過誰又能說得準之後會不會用到呢?)

「中心思想」分析

  • 正常運行的必要條件

    ​ 本模式中,經過ReadWriteLock類中的兩個獲取鎖和兩個釋放鎖的方法來模擬了synchronized關鍵字獲取實例的鎖和釋放實例的鎖這兩個過程,從而在邏輯上保證了本模式在線程安全方面與synchronized關鍵字保護的方法徹底相同。所以在安全性和生存性兩方面,本模式很好地完成了。

  • 提高性能的必要條件

    ​ 本模式中,經過找到讀取和寫入交匯的四種狀況中的讀讀無衝突的狀況,而且實現讀取鎖和寫入鎖的分離,實現了多線程同時讀取的效果,以此來提升頻繁讀取或是「重讀取」的程序的性能。
    ​ 同時,咱們不難發現,關於多線程同步控制的代碼都封裝在ReadWriteLock類中,其餘部分直接調用便可,無需進行同步控制,提升了可複用性。

Future 模式

Future模式特色

我從本模式中先提取出兩個最關鍵的核心代碼展現一下。

Data data = host.request(10, ‘A’);

host.request方法是啓動一個新線程來執行請求。可是在這行代碼中該方法的返回值,不是新線程執行獲得的最後結果,這個data只是一張「提貨單」、「預定券」

先返回「提貨單」的意義在於這個返回值能夠當即獲得,不用等待請求處理線程返回最後結果。在「作蛋糕」的期間,咱們能夠作一些別的和「蛋糕」無關的事情,等到「蛋糕作好了」咱們再回去取「蛋糕」。

data.getContent();

上面這句代碼就是線程「取蛋糕」的動做。這個方法的返回值是真正的「能吃的蛋糕」。

手搓Future模式代碼

類圖

Main類發出請求給Host類,Host類接收到請求後馬上製造一個FutureData類的實例看成提貨券返回給Main類,同時Host類馬上啓動一個新線程來處理請求(假設此處請求處理須要花費至關長時間),最後處理結果獲得RealData類(蛋糕)。

Main類

Main類中,向Host類發出了三個請求。以後Main線程就去作別的工做了,咱們這裏用sleep(2000)來模擬。作完別的工做以後,Main線程輸出請求的結果。

Host類

第一個紅線處,經過Future這個FutureData類的實例(共享對象),將Main線程(買蛋糕的人)和realdata(蛋糕)創建起了,超越「時空」的聯繫。

爲何說「時空」呢?我本身的理解這個模式,就是在主線程獲得提貨券後,主線程無論在什麼時候何地(這裏的空間是抽象空間,也即主線程不在處理請求線程的」線程空間"內)均可以在結果計算出來後即時獲取結果。

第二個紅線處,使用了一個不太經常使用的語法模式——匿名內部類。讀者沒必要對這個語法熟練掌握,只須要知道在示例程序裏,這個類新建了一個處理請求的線程實例並讓新的線程運行起來去處理請求便可。(count和c變量前面都加上final關鍵字是匿名內部類的要求,瞭解便可)

說到這裏,對於每一個新的請求都啓動一個新的線程來處理是另外一個多線程設計模式——Thread-Per-Message模式。這個模式較爲簡單,感興趣的讀者能夠自行學習瞭解一下,這裏再也不贅述了。

FutureData類

第一個紅線處,這裏設計的又是一個新的多線程設計模式——Balk模式。Balk模式的「中心思想」是不要我就走了。即當有多個線程時,其中一個線程已經完成了請求,那麼別的線程來要完成請求時,這個模式就經過if條件告訴線程:「我已經完成個人請求了,不用你再來工做了,你能夠走了。」,經過return將線程返回回去。

第2、三個紅線處,即在請求處理線程完成「蛋糕」的交付以後(this.realdata = realdata;),將ready字段置true,代表「蛋糕」已經隨時能夠取走了。而後通知全部等待線程。

第4、五個紅線處,使用守護模式,以沒有ready做爲守護條件,即若是「蛋糕」尚未作好,「取蛋糕」的線程就要wait。不然經過getContent方法返回回去。

RealData類

第一個紅線處,這個String字段在本示例程序中表明「蛋糕」。

第二個紅線處,示例程序中用sleep來模擬耗時很長的請求處理過程。

運行結果

經過結果輸出來看,在主線程執行其餘工做的時候,與此同時請求正在被處理,這樣極大地提升了處理效率。

模式分析

  • 提升吞吐量

    ​ 單核CPU中,純計算過程是沒法提升吞吐量的。其餘狀況都可。

  • 異步方法調用

    ​ 經過Thread-Per-Message模式經過新建線程,模擬實現了異步。可是Thread-Per-Message模式沒法接收返回值。

  • 「準備」和「使用」返回值的分離

    ​ 爲了解決Thread-Per-Message模式沒法接收返回值的尷尬局面,Future模式橫空出世。Future模式經過將準備返回值(返回提貨券)和使用返回值(調用getContent方法)分離,即解決了異步調用沒法接收返回值的問題,又提升了性能。

與生產者-消費者模式有區別嗎?

答案是有。

生產者-消費者模式你們都很熟悉,經過一個tray來將生產者生產產品(有的人將其對應爲本模式的請求處理過程)和消費者使用產品(有的人將其對應爲本模式的使用返回值過程)分離開來。目前來看,沒有什麼區別。

可是,咱們仔細想想,Future模式經過一張提貨券將「生產者「和」消費者「創建起來一對一的獨一無二的聯繫。也就是說我有這個」蛋糕」的提貨券,我只能取我這個本身的「蛋糕」,而不能取「蛋糕店」裏作好的別人的「蛋糕」。說到這裏,相信你們都已經發現本模式與生產者-消費者模式最大的區別了吧。

模式拓展

  • 不讓主線程久等的Future角色

    ​ 在示例程序中,若是FutureData的getContent方法被調用時,RealData類的實例尚未建立完成,則要主線程wait建立完成,有時這也會對主線程的效率形成損失。

    ​ 因此,爲了不這種狀況的發生,咱們能夠將守護模式換成Balk模式,即主線程來「取蛋糕」時,若「蛋糕」還沒作好,就讓主線程返回,再等一下子。這樣主線程能夠繼續進行其餘工做,過必定時間後再回來「取蛋糕」。

  • 會發生變化的Future角色

    ​ 一般狀況下,返回值只會被設置到Future角色中一次。可是在有時須要不斷反覆設置返回值時,能夠考慮給Future角色賦予「當前返回值」,即這個返回值會不斷隨時間而改變。

    ​ 例如:在經過網絡獲取圖像數據時,能夠在最開始獲取圖像的長和寬,接着獲取模糊圖像數據,在獲取清晰圖像數據。此時,這個不斷變化的Future角色可能會大有用處。

模式思考

在課上,老師提示我,是否能夠用簡單的方法實現主動返回值的Future模式。

目前,我只想到使用回調模式,在Future模式返回值設置好後,經過Host類回調主線程。不過,使用這種方式會致使Main類裏多出不少與多線程同步處理相關的代碼,致使Main類變的臃腫,並且整個模式可複用性也會下降。

我在想出好的解決辦法以後會及時更新本文,向你們展現。同時也歡迎各位讀者有好的解決辦法在評論區留言。

Future模式「中心思想」

  • 正常運行必要條件

    本模式相似生產者-消費者的邏輯,將處理與請求分離,分離的同時創建起超越「時空」的聯繫,保證了最後結果傳輸的準確性。

  • 提升性能必要條件

    經過將「準備」返回值和「使用」返回值分離,將主線程從漫長的請求處理過程解放出來,讓主線程在請求處理期間,能夠作別的工做,提升性能。

偉大的Concurrent包!

RW Lock模式

JAVA提供了java.util.concurrent.locks包來提供讀寫鎖的實現。這個包裏的ReentrantReadWriteLock類實現了ReadWriteLock接口。這個包的實現原理即爲上述手搓RW-Lock模式代碼所講解的原理和實現。具體使用方法很簡單,在理解原理以後使用很簡單,就很少贅述了。

Future模式

JAVA提供了java.util.concurrent.Future接口至關於本模式中的Future角色。其中java.util.concurrent.FutureTask類是實現了Future接口的標準類,主要有get(獲取返回值)、set(設置返回值)、cancel(中斷請求處理運行)和setException(設置異常)四個方法。

其原理和上述Future模式的手搓代碼原理徹底一致,相信你們徹底理解上述講解後,對這些concurrent包的使用必定會更加駕輕就熟!!

示例程序代碼

  • RW Lock模式
public class Main {
    public static void main(String[] args) {
        Data data = new Data(10);
        Thread Reader1 = new ReaderThread(data);
        Reader1.start();
        Thread Reader2 = new ReaderThread(data);
        Reader2.start();
        Thread Reader3 = new ReaderThread(data);
        Reader3.start();
        Thread Reader4 = new ReaderThread(data);
        Reader4.start();
        Thread Reader5 = new ReaderThread(data);
        Reader5.start();
        Thread Reader6 = new ReaderThread(data);
        Reader6.start();
        Thread Writer1 = new WriterThread(data, "ABCDEFGHIJKLMNOPQTSTUVWXYZ");
        Writer1.start();
        Thread Writer2 = new WriterThread(data, "abcdefghijklmnopqrstuvwxyz");
        Writer2.start();
        Scanner input = new Scanner(System.in);
        String end  = input.nextLine();
        while (end.equals("")) { end  = input.nextLine(); }
        Reader1.interrupt();
        Reader2.interrupt();
        Reader3.interrupt();
        Reader4.interrupt();
        Reader5.interrupt();
        Reader6.interrupt();
        Writer1.interrupt();
        Writer2.interrupt();
    }
}


public class Data {
    private final char[] buffer;
    private ReadWriteLock lock = new ReadWriteLock();
    public Data(int size) {
        this.buffer = new char[size];
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = '*';
        }
    }
    public synchronized char[] read() throws InterruptedException {
        lock.readLock();
        try {
            return doRead();
        } finally {
            lock.readUnlock();
        }
    }
    public synchronized void write(char c) throws InterruptedException {
        lock.writeLock();
        try {
            doWrite(c);
        } finally {
            lock.writeUnlock();
        }
    }
    private char[] doRead() {
        char[] newbuf = new char[buffer.length];
        for (int i = 0; i < buffer.length; i++) {
            newbuf[i] = buffer[i];
        }
        slowly();
        return newbuf;
    }
    private void doWrite(char c) {
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = c;
            slowly();
        }
    }
    private void slowly() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
        }
    }
}

//此處爲性能測試代碼(即執行20次讀取,並統計時間)
public class ReaderThread extends Thread {
    private final Data data;
    public ReaderThread(Data data) {
        this.data = data;
    }
    public void run() {
        try {
            long begin = System.currentTimeMillis();
            for (int i = 0; i < 20; i++) {
                char[] readbuf = data.read();
                System.out.println(Thread.currentThread().getName() + " reads " + String.valueOf(readbuf));
            }
            long time = System.currentTimeMillis() - begin;
            System.out.println(Thread.currentThread().getName() + ":time = " + time);
        } catch (InterruptedException e) {
        }
    }
}


import java.util.Random;

public class WriterThread extends Thread {
    private static final Random random = new Random();
    private final Data data;
    private final String filler;
    private int index = 0;
    public WriterThread(Data data, String filler) {
        this.data = data;
        this.filler = filler;
    }
    public void run() {
        try {
            while (true) {
                char c = nextchar();
                data.write(c);
                Thread.sleep(random.nextInt(3000));
            }
        } catch (InterruptedException e) {
        }
    }
    private char nextchar() {
        char c = filler.charAt(index);
        index++;
        if (index >= filler.length()) {
            index = 0;
        }
        return c;
    }
}


public final class ReadWriteLock {
    private int readingReaders = 0; // (A)…實際正在讀取中的線程個數
    private int waitingWriters = 0; // (B)…正在等待寫入的線程個數
    private int writingWriters = 0; // (C)…實際正在寫入中的線程個數
    private boolean preferWriter = true; // 若寫入優先,則爲true

    public synchronized void readLock() throws InterruptedException {
        while (writingWriters > 0 || (preferWriter && waitingWriters > 0)) {
            wait();
        }
        readingReaders++;                       // (A) 實際正在讀取的線程個數加1
    }

    public synchronized void readUnlock() {
        readingReaders--;                       // (A) 實際正在讀取的線程個數減1
        preferWriter = true;
        notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
        waitingWriters++;                       // (B) 正在等待寫入的線程個數加1
        try {
            while (readingReaders > 0 || writingWriters > 0) {
                wait();
            }
        } finally {
            waitingWriters--;                   // (B) 正在等待寫入的線程個數減1
        }
        writingWriters++;                       // (C) 實際正在寫入的線程個數加1
    }

    public synchronized void writeUnlock() {
        writingWriters--;                       // (C) 實際正在寫入的線程個數減1
        preferWriter = false;
        notifyAll();
    }
}
  • Future模式
public class Main {
    public static void main(String[] args) {
        System.out.println("main BEGIN");
        Host host = new Host();
        Data data1 = host.request(10, 'A');
        Data data2 = host.request(20, 'B');
        Data data3 = host.request(30, 'C');

        System.out.println("main otherJob BEGIN");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println("main otherJob END");

        System.out.println("data1 = " + data1.getContent());
        System.out.println("data2 = " + data2.getContent());
        System.out.println("data3 = " + data3.getContent());
        System.out.println("main END");
    }
}


public class Host {
    public Data request(final int count, final char c) {
        System.out.println("    request(" + count + ", " + c + ") BEGIN");

        // (1) 建立FutureData的實例
        final FutureData future = new FutureData();

        // (2) 啓動一個新線程,用於建立RealData的實例
        new Thread() {
            public void run() {
                RealData realdata = new RealData(count, c);
                future.setRealData(realdata);
            }
        }.start();

        System.out.println("    request(" + count + ", " + c + ") END");

        // (3) 返回FutureData的實例
        return future;
    }
}


public interface Data {
    public abstract String getContent();
}


public class RealData implements Data {
    private final String content;
    public RealData(int count, char c) {
        System.out.println("        making RealData(" + count + ", " + c + ") BEGIN");
        char[] buffer = new char[count];
        for (int i = 0; i < count; i++) {
            buffer[i] = c;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
        System.out.println("        making RealData(" + count + ", " + c + ") END");
        this.content = new String(buffer);
    }
    public String getContent() {
        return content;
    }
}


public class FutureData implements Data {
    private RealData realdata = null;
    private boolean ready = false;
    public synchronized void setRealData(RealData realdata) {
        if (ready) {
            return;     // balk
        }
        this.realdata = realdata;
        this.ready = true;
        notifyAll();
    }
    public synchronized String getContent() {
        while (!ready) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        return realdata.getContent();
    }
}

參考資料:《圖解JAVA多線程設計模式》

相關文章
相關標籤/搜索