日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

Java 多线程设计模式

發(fā)布時間:2023/12/16 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java 多线程设计模式 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

多線程程序的評量標準

  • 安全性:不損壞對象
    不安全是指,對象的狀態(tài)處于非預期狀態(tài),比如賬戶余額變成了負值

  • 生存性:進行必要的處理
    生存性是指:程序能正常運行,可進行必要的處理,影響生存性的典型問題有出現(xiàn)死鎖

  • 復用性:可再利用類
    復用性是指代碼重用,若復用性好,可減少大量重復代碼

  • 性能:能快速,大量進行處理
    性能有兩個方面的考慮因素:吞吐量和響應性,客戶端程序比較重視響應性,服務端程序更重視吞吐量,吞吐量是指單位時間內(nèi)完成的任務,響應性是指提交任務后多長時間內(nèi)能收到程序的反饋。比如說我們在QQ時,經(jīng)常感覺QQ卡,這便是響應性問題。

  • 其中安全性和生存性是必要的,如果安全性和生存性都沒有保證,就無所謂別的考量了。復用性和性能決定了程序的質(zhì)量

    《多線程設(shè)計模式》一共講了12個設(shè)計模式,列舉如下。

    1. Single Threaded Execution

    只允許單個線程執(zhí)行對象的某個方法,以保護對象的多個狀態(tài)。
    實現(xiàn)時需用synchronized修飾引用受保護的狀態(tài)的方法,這樣就只能有單個線程訪問該方法,其它線程由于不能獲取鎖而等待,因為只有一個線程去訪問受保護狀態(tài)變量,故此不需要擔心該狀態(tài)變量被別的線程修改。
    也可以用synchronized修飾代碼塊來保護狀態(tài)字段。

    示例程序:

    public class Gate {private String _name = "NoBody";private String _where = "NoBody"; public synchronized void pass(String name, String where) {_name = name;_where = where; check();}private void check() {if (_name.charAt(0) != _where.charAt(0)) {System.out.println("*****************Broken**************");}}}

    如果這里不用synchronized修飾pass方法,多線程環(huán)境下會有多個線程同時執(zhí)行pass方法,容易造成狀態(tài)不一致,引入安全性問題。

    適用場景:

    多線程環(huán)境下如果狀態(tài)變量(可能有多個狀態(tài)變量,并且它們之間是相關(guān)的)被多個線程訪問,并且可能會發(fā)生變化,此時需要將狀態(tài)變量封裝起來(可以用類進行封裝),并將訪問這些狀態(tài)變量的方法用synchronized進行保護??梢杂胹ynchronized修飾方法,也可以修飾代碼塊。

    注意事項:

    一定要注意synchronized是通過獲取哪個鎖來保護狀態(tài)變量,如果保護狀態(tài)變量時使用不同的鎖對象,那么多個線程仍然可以同時訪問被保護的狀態(tài)變量,尤其是保護多個相關(guān)狀態(tài)變量時一定要記得用同一個鎖對象。synchronized修飾方法時,獲取的鎖對象是synchronied方法所在類的實例,synchorized修飾this時,獲取的鎖對象也是當前類的實例。
    synchronized修飾符不會被繼承,也就是說我們覆蓋父類的synchronized方法時,如果不添加synchronized修飾符,就不能保護狀態(tài)變量,因此覆蓋父類方法時,如果想保護某些狀態(tài)變量,記得添加synchronized修飾符。

    2. Immutable

    在single threaded executetion這個模式里我們使用了synchronized來保護需要保護的狀態(tài)變量,因為這些狀態(tài)可能會變化,如果不保護的話,可能會破壞對象。但是用synchronized保護變量也帶來了性能問題,因為獲取鎖需要時間,并且如果多個線程競爭鎖的話,會讓某些線程進入這個鎖的條件隊列,暫停執(zhí)行,這樣會降低性能。
    如果狀態(tài)根本不會發(fā)生變化,就不需要用鎖保護,這就是Immutable模式。

    示例程序:

    public final class Person {private final String _name;private final String _address;public Person(String name, String address) {_name = name;_address = address;}public String getName() {return _name;}public String getAddress() {return _address;}@Overridepublic String toString() {return "Person [_name=" + _name + ", _address=" + _address + "]";}}

    Person類用final修飾,防止被繼承。
    _name和_address都用final修飾,防止被修改,只能在定義時初始化,或者在構(gòu)造器里初始化,Person類也只提供了對這些狀態(tài)字段的get方法,故此外界調(diào)用該類的實例時無法修改這些狀態(tài)。

    適用場景:

    對于那些不會變化的狀態(tài)可用Immutable類進行封裝,這樣可避免用鎖同步,從而提高性能。

    注意事項:

    String就是一個Immutable類,與之相對應的StringBuilder或者StringBuffer是muttable類。我們在設(shè)計類時,針對那些需要共享并且訪問很頻繁的實例,可將其設(shè)置為Immutalbe類,如果在少數(shù)情況下它的狀態(tài)也可能會變化,可為之設(shè)計相對應的muttable類,像String和StringBuffer的關(guān)系一樣。
    StringBuilder是非線程安全的,StringBuffer是線程安全的,String也是線程安全的,因為它是immutable類。
    java里的包裝器類全是immutable類。

    3. Guarded Suspension

    當我們調(diào)用對象某個的某個方法時,可能對象當前狀態(tài)并不滿足執(zhí)行的條件,于是需要等待,這就是Guarded Suspension模式。只有當警戒條件滿足時,才執(zhí)行,否則等待,另外對象必須有改變其狀態(tài)的方法。

    示例程序:

    public class RequestQueue {private final LinkedList<Request> _queue = new LinkedList<Request>();public synchronized Request getRequest() {while (_queue.size() <= 0) {try {wait();} catch (InterruptedException e) {}}return _queue.removeFirst();}public synchronized void putRequest(Request request) {_queue.add(request);notifyAll();}}

    _queue.size()>0 便是警戒條件,只有當_queue.size()>0才能調(diào)用_queue.removeFirst(),當警戒條件不滿足時,需要wait。
    putRequest方法可以改變RequestQueue的狀態(tài),使getRequest方法里的警戒條件滿足。

    適用場景:

    某個調(diào)用者的方法在執(zhí)行時如果希望當狀態(tài)不滿足時等待狀態(tài)滿足后再執(zhí)行,如果狀態(tài)滿足,則立即執(zhí)行,可考慮使用Guarded Suspension模式。

    注意事項:

    Guarded Suspension里的警戒方法(等待狀態(tài)成立才執(zhí)行的方法)是同步阻塞的,狀態(tài)不滿足時,調(diào)用該方法的線程會阻塞。
    Guarded Suspension里的狀態(tài)變更方法里須記得在狀態(tài)變更后,調(diào)用notifyAll,使得調(diào)用警戒方法的線程可恢復執(zhí)行。

    4. Balking

    Balking模式與Guarded Suspension模式相似,都是在對象狀態(tài)不符合要求時需要進行一些處理,不過Guared Suspension在狀態(tài)不滿足要求時,會等待并阻塞線程,而Balking模式是直接返回,并不等待。調(diào)用者可暫時先做別的工作,稍后再來調(diào)用該對象的方法。

    示例程序:

    public class Data {private final String _file_name;private String _content;private boolean _changed;public Data(String filename, String conetent) {_file_name = filename;_content = conetent;_changed = false;}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 {Writer writer = new FileWriter(_file_name);writer.write(_content);writer.close();}}

    save方法里首先檢測字符串是否有變化,如果沒有變化則立即返回,否則才保存字符串,這樣可避免不必要的IO,提高性能。
    上述實例中的警戒條件是_changed為true

    適用場景:

    不想等待警戒條件成立時,適合使用Balking模式。
    警戒條件只有第一次成立時,適合使用Balking模式。

    注意事項:

    該模式并不會等待警戒條件成立,當警戒條件不成立時直接返回了,故此改變狀態(tài)的方法也就不需要調(diào)用notifyAll方法。
    另外注意不管是警戒條件方法還是改變狀態(tài)的方法都需要用synchronized同步,因為這里封裝了多個數(shù)據(jù),一個用于判斷警戒條件的狀態(tài),還有真實數(shù)據(jù)。

    5. Producer-Consumer

    生產(chǎn)者消費者問題是操作系統(tǒng)里非常經(jīng)典的同步問題,生產(chǎn)者生產(chǎn)好數(shù)據(jù)后,放到緩沖區(qū),消費者從緩沖區(qū)取出數(shù)據(jù)。但是當緩沖區(qū)滿了的時候,生產(chǎn)者不可再將生產(chǎn)好的數(shù)據(jù)放到緩沖區(qū),當緩沖區(qū)沒有數(shù)據(jù)的時候消費者不可再從緩沖區(qū)里取出數(shù)據(jù)。
    解決生產(chǎn)者消費者問題的方案稱之為生產(chǎn)者消費者模式,在該模式里可能有多個生產(chǎn)者,多個消費者,生產(chǎn)者和消費者都有獨立的線程。其中最關(guān)鍵的是放置數(shù)據(jù)的緩沖區(qū),生產(chǎn)者和消費者在操作緩沖區(qū)時都必須同步,生產(chǎn)者往緩沖區(qū)放置數(shù)據(jù)時,如果發(fā)現(xiàn)緩沖區(qū)已滿則等待,消費者從緩沖區(qū)取數(shù)據(jù)時如果發(fā)現(xiàn)緩沖區(qū)沒有數(shù)據(jù),也必須等待。

    示例程序:

    public class Table {private final String[] _buffer;private int _tail;private int _head;private int _count;public Table(int count) {_buffer = new String[count];_head = 0;_tail = 0;_count = 0;}public synchronized void put(String cake) throws InterruptedException {while (_count >= _buffer.length) {wait();}_buffer[_tail] = cake;_tail = (_tail + 1) % _count;_count++;notifyAll();}public synchronized String take() throws InterruptedException {while (_count <= 0) {wait();}String cake = _buffer[_head];_head = (_head + 1) % _count;_count--;notifyAll();return cake;} }

    這里table扮演的便是數(shù)據(jù)緩沖區(qū)的角色,當消費者調(diào)用take取數(shù)據(jù)時,如果發(fā)現(xiàn)數(shù)據(jù)數(shù)目少于0時,便會等待,當生產(chǎn)者調(diào)用put放數(shù)據(jù)時,如果發(fā)現(xiàn)數(shù)據(jù)數(shù)目大于緩沖區(qū)大小時,也會等待。

    適用場景:

    當程序里有多個生產(chǎn)者角色或者多個消費者角色操作同一個共享數(shù)據(jù)時,適合用生產(chǎn)者消費者模式。比如下載模塊,通常會有多個下載任務線程(消費者角色),用戶點擊下載按鈕時產(chǎn)生下載任務(生產(chǎn)者角色),它們會共享任務隊列。

    注意事項:

    不管是生產(chǎn)方法還是消費方法,當警戒條件不滿足時,一定要等待,警戒條件滿足后執(zhí)行完放置數(shù)據(jù)邏輯或者取出數(shù)據(jù)邏輯后一定要調(diào)用notifyAll方法,使得其它線程恢復運行。

    6. Read-Write Lock

    先前的幾個多線程設(shè)計模式里,操作共享數(shù)據(jù)時,不管如何操作數(shù)據(jù)一律采取互斥的策略(除了Immutable模式),即只允許一個線程執(zhí)行同步方法,其它線程在共享數(shù)據(jù)的條件隊列里等待,只有執(zhí)行同步方法的線程執(zhí)行完同步方法后被阻塞的線程才可在獲得同步鎖后繼續(xù)執(zhí)行。
    這樣效率其實有點低,因為讀操作和讀操作之間并不需要互斥,兩個讀線程可以同時操作共享數(shù)據(jù),讀線程和寫線程同時操作共享數(shù)據(jù)會有沖突,兩個寫線程同時操作數(shù)據(jù)也會有沖突。

    示例程序:

    Data類

    public class Data {private final char[] _buffer;private final ReadWriteLock _lock = new ReadWriteLock();public Data(int size) {_buffer = new char[size];for (int i = 0; i < size; i++)_buffer[i] = '*';}public char[] read() throws InterruptedException {_lock.readLock();try {return doRead();} finally {_lock.readUnlock();}}public void write(char c) throws InterruptedException {_lock.writeLock();try {doWrite(c);} finally {_lock.writeUnock();}}private char[] doRead() {char[] newbuf = new char[_buffer.length];for (int i = 0; i < newbuf.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(500);} catch (InterruptedException e) {e.printStackTrace();}}}

    ReadWriteLock類

    public class ReadWriteLock {private int _reading_readers = 0;private int _waiting_writers = 0;private int _writing_writers = 0;private boolean _prefer_writer = true;public synchronized void readLock() throws InterruptedException {while (_writing_writers > 0 || (_prefer_writer && _waiting_writers > 0)) {wait();}_reading_readers++;}public synchronized void readUnlock() {_reading_readers--;_prefer_writer = true;notifyAll();}public synchronized void writeLock() throws InterruptedException {_waiting_writers++;try {while (_reading_readers > 0 || _writing_writers > 0)wait();} finally {_waiting_writers--;}_writing_writers++;}public synchronized void writeUnock() {_writing_writers--;_prefer_writer = false;notifyAll();}}

    這里為讀寫鎖設(shè)置了單獨的類ReadWriteLock,ReadWriteLock提供了4個方法readLock,readUnlock,writeLock,writeUnlock。
    讀線程在讀取共享數(shù)據(jù)時,先調(diào)用readLock方法獲取讀鎖,然后使用try塊讀取共享數(shù)據(jù)并在finnally塊中調(diào)用readUnlock釋放讀鎖。寫線程在寫入共享數(shù)據(jù)時,先調(diào)用writeLock方法獲取寫鎖,然后使用try塊寫入共享數(shù)據(jù)并在finnally塊中調(diào)用writeUnlock方法釋放寫鎖。
    實現(xiàn)ReadWriteLock時使用了_waiting_writers和_prefer_writer,其實如果不采用這兩個字段也能實現(xiàn)讀寫鎖,但是使用了_prefer_writer后可以讓讀線程以及寫線程不致于饑餓。每次讀線程調(diào)用完readUnlock后設(shè)置_prefer_writer為true,此時如果有寫線程等待寫入,便可恢復執(zhí)行,而不是由其它讀線程繼續(xù)執(zhí)行。每次寫線程調(diào)用完writeUnlock后,_prefer_writer為false,此時等待讀取的線程可恢復執(zhí)行。

    適用場景:

    操作共享數(shù)據(jù)的讀線程明顯多于寫線程時可采用讀寫鎖模式提高程序性能。

    注意事項:

    Java 5的concurrent包里已經(jīng)有ReadWriteLock接口,對應的類有ReentrantReadWriteLock,沒必要自己實現(xiàn)ReadWriteLock類。并發(fā)庫里的類都是經(jīng)過測試的穩(wěn)定的類,并且性能也會比自己寫的類要高,因此我們應該優(yōu)先選擇并發(fā)庫里的類。

    7. Thread-Per-Message

    實現(xiàn)某個方法時創(chuàng)建新線程去完成任務,而不是在本方法里完成任務,這樣可提高響應性,因為有些任務比較耗時。

    示例程序:

    public class Host {private final Handler _handler=new Handler();public void request(final int count, final char c){new Thread(){public void run(){_handler.handle(count, c);}}.start();} }

    實現(xiàn)Host類的方法時,新建了一個線程調(diào)用Handler對象處理request請求。
    每次調(diào)用Host對象的request方法時都會創(chuàng)建并啟動新線程,這些新線程的啟動順序不是確定的。

    適用場景:

    適合在操作順序無所謂時使用,因為請求的方法里新建的線程的啟動順序不是確定的。
    在不需要返回值的時候才能使用,因為request方法不會等待線程結(jié)束才返回,而是會立即返回,這樣得不到請求處理后的結(jié)果。

    注意事項:

    每次調(diào)用都會創(chuàng)建并啟動一個新線程,對新建線程沒有控制權(quán),實際應用中只有很簡單的請求才會用Thread-Per-Message這個模式,因為通常我們會關(guān)注返回結(jié)果,也會控制創(chuàng)建的線程數(shù)量,否則系統(tǒng)會吃不消。

    8. Worker Thread

    在Thread-Per-Message模式里,每次函數(shù)調(diào)用都會啟動一個新線程,但是啟動新線程的操作其實是比較繁重的,需要比較多時間,系統(tǒng)對創(chuàng)建的線程數(shù)量也會有限制。我們可以預先啟動一定數(shù)量的線程,組成線程池,每次函數(shù)調(diào)用時新建一個任務放到任務池,預先啟動的線程從任務池里取出任務并執(zhí)行。這樣便可以控制線程的數(shù)量,也避免了每次啟動新線程的高昂代價,實現(xiàn)了資源重復利用。

    示例程序:

    Channel類:

    public class Channel {private static final int MAX_REQUEST = 100;private final Request[] _request_queue;private int tail;private int head;private int count;private WorkerThread[] _thread_pool;public Channel(int threads) {_request_queue = new Request[MAX_REQUEST];tail = 0;head = 0;count = 0;_thread_pool = new WorkerThread[threads];for (int i = 0; i < threads; i++) {_thread_pool[i] = new WorkerThread("Worker-" + i, this);}}public void startWorkers() {for (int i = 0; i < _thread_pool.length; i++)_thread_pool[i].start();}public synchronized Request takeRequest() throws InterruptedException {while (count <= 0) {wait();}Request request = _request_queue[head];head = (head + 1) % _request_queue.length;count--;notifyAll();return request;}public synchronized void putRequest(Request request)throws InterruptedException {while (count >= _request_queue.length) {wait();}_request_queue[tail] = request;tail = (tail + 1) % _request_queue.length;count++;notifyAll();}}

    WorkerThread類:

    public class WorkerThread extends Thread {private final Channel _channel;public WorkerThread(String name, Channel channel) {super(name);_channel = channel;}@Overridepublic void run() {while (true) {Request request;try {request = _channel.takeRequest();request.execute();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}

    channel類集成了線程池和任務池,對外提供了startWorkers方法,外界可調(diào)用該方法啟動所有工作線程,然后通過putRequest方法向任務池添加任務,工作者線程會自動從任務池里取出任務并執(zhí)行。

    適用場景:

    和Thread-Per-Message模式一樣,Worker Thread模式實現(xiàn)了invocation和exectution的分離,即調(diào)用和執(zhí)行分離,調(diào)用者調(diào)用方法運行在一個線程,任務的執(zhí)行在另一個線程。調(diào)用者調(diào)用方法后可立即返回,提高了程序的響應性。另外也正是因為調(diào)用和執(zhí)行分離了,我們可以控制任務的執(zhí)行順序,還可以取消任務,還能分散處理,將任務交給不同的機器執(zhí)行,如果沒有將調(diào)用和執(zhí)行分離,這些特性是無法實現(xiàn)的。
    適合有大量任務并且還需要將任務執(zhí)行分離的程序,比如象應用分發(fā)類App,需要經(jīng)常和服務器通信獲取數(shù)據(jù),并且通信消息可能還有優(yōu)先級。

    注意事項:

    注意控制工作者線程的數(shù)量,如果過多,那么會有不少工作者線程并沒有工作,會浪費系統(tǒng)資源,如果過少會使得任務池里塞滿,導致其它線程長期阻塞??筛鶕?jù)實際工作調(diào)整線程數(shù)量,和任務池里的最大任務池數(shù)。
    如果worker thread只有一條,工人線程處理的范圍就變成單線程了,可以省去共享互斥的必要。通常GUI框架都是這么實現(xiàn)的,操作界面的線程只有一個,界面元素的方法不需要進行共享互斥。如果操作界面的線程有多個,那么必須進行共享互斥,我們還會經(jīng)常設(shè)計界面元素的子類,子類實現(xiàn)覆蓋方法時也必須使用synchronized進行共享互斥,引入共享互斥后會引入鎖同步的開銷,使程序性能降低,并且如果有不恰當?shù)墨@取鎖的順序,很容易造成死鎖,這使得GUI程序設(shè)計非常復雜,故此GUI框架一般都采用單線程。

    Java 5的并發(fā)包里已經(jīng)有線程池相關(guān)的類,無需自己實現(xiàn)線程池??墒褂肊xecutors的方法啟動線程池,這些方法包括newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool等等。

    9. Future

    在Thread-Per-Message模式和Worker Thread模式里,我們實現(xiàn)了調(diào)用和執(zhí)行分離。但是通常我們調(diào)用一個函數(shù)是可以獲得返回值的,在上述兩種模式里,雖然實現(xiàn)了調(diào)用和執(zhí)行相分離,但是并不能獲取調(diào)用執(zhí)行的返回結(jié)果。Future模式則可以獲得執(zhí)行結(jié)果,在調(diào)用時返回一個Future,可以通過Future獲得真正的執(zhí)行結(jié)果。

    示例程序:

    Host類

    public class Host {public Data request(final int count, final char c) {System.out.println(" request (" + count + ", " + c + " ) BEGIN");final FutureData future = new FutureData();new Thread() {@Overridepublic void run() {RealData realData = new RealData(count, c);future.setRealData(realData);}}.start();return future;}}

    Data接口

    public interface Data {public String getContent(); }

    FutureData類

    public class FutureData implements Data {private boolean _ready = false;private RealData _real_data = null;public synchronized void setRealData(RealData realData) {if (_ready)return;_real_data = realData;_ready = true;notifyAll();}@Overridepublic synchronized String getContent() {while (!_ready) {try {wait();} catch (InterruptedException e) {}}return _real_data.getContent();}}

    RealData類

    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 (Exception e) {}}System.out.println(" making Real Data(" + count + "," + c + ") END");_content = new String(buffer);}@Overridepublic String getContent() {return _content;}}

    適用場景:

    如果既想實現(xiàn)調(diào)用和執(zhí)行分離,又想獲取執(zhí)行結(jié)果,適合使用Future模式。
    Future模式可以獲得異步方法調(diào)用的”返回值”,分離了”準備返回值”和”使用返回值”這兩個過程。

    注意事項:

    Java 5并發(fā)包里已經(jīng)有Future接口,不僅能獲得返回結(jié)果,還能取消任務執(zhí)行。當調(diào)用ExecutorService對象的submit方法向任務池提交一個Callable任務后,可獲得一個Future對象,用于獲取任務執(zhí)行結(jié)果,并可取消任務執(zhí)行。

    10. Two-Phase Termination

    這一節(jié)介紹如何停止線程,我們剛開始學習線程時,可能很容易犯的錯就是調(diào)用Thread的stop方法停止線程,該方法確實能迅速停止線程,并會讓線程拋出異常。但是調(diào)用stop方法是不安全的,如果該線程正獲取了某個對象的鎖,那么這個鎖是不會被釋放的,其他線程將繼續(xù)被阻塞在該鎖的條件隊列里,并且也許線程正在做的工作是不能被打斷的,這樣可能會造成系統(tǒng)破壞。從線程角度看,只有執(zhí)行任務的線程本身知道該何時恰當?shù)耐V箞?zhí)行任務,故此我們需要用Two-Phase Termination模式來停止線程,在該模式里如果想停止某個線程,先設(shè)置請求線程停止的標志為true,然后調(diào)用Thread的interrupt方法,在該線程里每完成一定工作會檢查請求線程停止的標志,如果為true,則安全地結(jié)束線程。

    示例程序:

    public class CountupThread extends Thread {private long counter = 0;private volatile boolean _shutdown_requested = false;public void shutdownRequest() {_shutdown_requested = true;interrupt();}public boolean isShutdownRequested() {return _shutdown_requested;}@Overridepublic void run() {try {while (!_shutdown_requested) {doWork();}} catch (InterruptedException e) {e.printStackTrace();} finally {doShutdown();}}private void doWork() throws InterruptedException {counter++;System.out.println("doWork: counter = " + counter);Thread.sleep(500);}private void doShutdown() {System.out.println("doShutDown: counter = " + counter);}}

    外界可調(diào)用shutdownRequest來停止線程。

    適用場景:

    需要停止線程時,可考慮使用Two-Phase Termination模式

    注意事項:

    我們在請求線程停止時,若只設(shè)置請求停止標志,是不夠的,因為如果線程正在執(zhí)行sleep操作,那么會等sleep操作執(zhí)行完后,再執(zhí)行到檢查停止標志的語句才會退出,這樣程序響應性不好。
    響應停止請求的線程如果只檢查中斷狀態(tài)(不是說我們設(shè)置的停止標志)也是不夠的,如果線程正在sleep或者wait,則會拋出InterruptedException異常,就算沒有拋出異常,線程也會變成中斷狀態(tài),似乎我們沒必要設(shè)置停止標志,只需檢查InterruptedException或者用isInterrupted方法檢查當前線程的中斷狀態(tài)就可以了,但是這樣做會引入潛在的危險,如果該線程調(diào)用的方法忽略了InterruptedException,或者該線程使用的對象的某個方法忽略了InterruptedException,而這樣的情況是很常見的,尤其是如果我們使用某些類庫代碼時,又不知其實現(xiàn),即使忽略了InterruptedException,我們也不知道,在這種情況下,我們無法檢查到是否有其它線程正在請求本線程退出,故此說設(shè)置終端標志是有必要的,除非能保證線程所引用的所有對象(包括間接引用的)不會忽略InterruptedException,或者能保存中斷狀態(tài)。

    中斷狀態(tài)和InterruptedException可以互轉(zhuǎn):

    • 中斷狀態(tài) -> InterruptedException

      if (Thread.interrupted) {throw new InterruptedException() }
    • InterruptedException -> 中斷狀態(tài)

      try {Thread.sleep(1000); } catch (InterruptedException e) {Thread.currentThread().interrupt(); }
    • InterruptedException -> InterruptedException

      InterruptedException savedInterruptException = null; ... try {Thread.sleep(1000); } catch (InterruptedException e) {savedInterruptException=e; } ... if(savedInterruptException != null )throw savedInterruptException;

    11. Thread-Specific Storage

    我們知道,如果一個對象不會被多個線程訪問,那么就不存在線程安全問題。Thread-Specific Storage模式就是這樣一種設(shè)計模式,為每個線程生成單獨的對象,解決線程安全問題。不過為線程生成單獨的對象這些細節(jié)對于使用者來說是隱藏的,使用者只需簡單使用即可。需要用到ThreadLocal類,它是線程保管箱,為每個線程保存單獨的對象。

    示例程序:

    Log類

    public class Log {private static ThreadLocal<TSLog> _ts_log_collection = new ThreadLocal<TSLog>();public static void println(String s) {getTSLog().println(s);}public static void close() {getTSLog().close();}private static TSLog getTSLog() {TSLog tsLog = _ts_log_collection.get();if (tsLog == null) {tsLog = new TSLog(Thread.currentThread().getName() + "-log.txt");_ts_log_collection.set(tsLog);}return tsLog;}}

    TSLog類

    public class TSLog {private PrintWriter _writer = null;public TSLog(String fileName) {try {_writer = new PrintWriter(fileName);} catch (FileNotFoundException e) {e.printStackTrace();}}public void println(String s) {_writer.write(s);}public void close() {_writer.close();} }

    適用場景:

    使用Thread-Specific Storgae模式可很好的解決多線程安全問題,每個線程都有單獨的對象,如果從ThreadLocal類里獲取線程獨有對象的時間遠小于調(diào)用對象方法的執(zhí)行時間,可提高程序性能。因此在日志系統(tǒng)里如果可以為每個線程建立日志文件,那么特別適合使用Thread-Specific Storage模式。

    注意事項:

    采用Thread-Specific Storage模式意味著將線程特有信息放在線程外部,在示例程序里,我們將線程特有的TSLog放在了ThreadLocal的實例里。通常我們一般將線程特有的信息放在線程內(nèi)部,比如建立一個Thread類的子類MyThread,我們聲明的MyThread的字段,就是線程特有的信息。因為把線程特有信息放在線程外部,每個線程訪問線程獨有信息時,會取出自己獨有信息,但是調(diào)試時會困難一些,因為有隱藏的context(當前線程環(huán)境), 程序以前的行為,也可能會使context出現(xiàn)異常,而是造成現(xiàn)在的bug的真正原因,我們比較難找到線程先前的什么行為導致context出現(xiàn)異常。

    設(shè)計多線程程序,主體是指主動操作的對象,一般指線程,客體指線程調(diào)用的對象,一般指的是任務對象,會因為重點放在“主體”與“客體”的不同,有兩種開發(fā)方式:

    • Actor-based 注重主體
    • Task-based 注重客體

    Actor-based 注重主體,偏重于線程,由線程維護狀態(tài),將工作相關(guān)的信息都放到線程類的字段,類似這樣

    class Actor extends Thread {操作者內(nèi)部的狀態(tài)public void run(){從外部取得任務,改變自己內(nèi)部狀態(tài)的循環(huán)} }

    Task-based注重客體,將狀態(tài)封裝到任務對象里,在線程之間傳遞這些任務對象,這些任務對象被稱為消息,請求或者命令。使用這種開發(fā)方式的最典型的例子是Worker Thread模式,生產(chǎn)者消費者模式。任務類似這樣:

    class Task implements Runnable{執(zhí)行任務所需的信息public void run(){ 執(zhí)行任務所需的處理內(nèi)容} }

    實際上這兩個開發(fā)方式是混用的,本人剛設(shè)計多線程程序時,總是基于Actor-based的思維方式,甚至在解決生產(chǎn)者消費者問題時也使用Actor-based思維方式,造成程序結(jié)構(gòu)混亂,因此最好按實際場景來,適合使用Actor-based開發(fā)方式的就使用Actor-based,適合Task-based開發(fā)方式的就使用Task-based。

    12. Active Object

    Active Object模式,也稱為Actor模式。Active Object即主動對象,它不僅擁有獨立線程,并且可以從外部接收異步消息,并能配合需要返回處理結(jié)果。這里的Active Object不是指一個對象,而是指將一群對象組織起來,對外表現(xiàn)為一個整體,這個整體擁有獨立線程,并能接收外部的異步消息,這個整體(Active Object)處理完異步消息后還可以返回結(jié)果給調(diào)用者。
    Future Pattern也能接收異步消息并返回處理結(jié)果,但是該模式聚焦在Future上,不是很關(guān)注線程主動執(zhí)行方面,而Activie Object將獨立線程,接收異步消息并返回處理結(jié)果這些方面看作一個整體。Active Object模式綜合利用了先前介紹的Producer-Consumer模式,Thread-Per-Message模式,Future模式等多線程設(shè)計模式。

    示例程序:

    代碼可上github下載: https://github.com/cloudchou/Multithread_ActiveObject

    類圖如下圖所示(請點擊看大圖):

    這是一個非常復雜的模式,ActiveObject接口里的每個方法對應MethodRequest的一個子類,每個方法的參數(shù)對應著MethodRequest的一個字段,因為ActiveObject的某些方法有返回值,故此設(shè)計了Result抽象類,表示返回值,為了讓調(diào)用和執(zhí)行分離,這里使用了Future模式,故此設(shè)計了三個類,Result,FutureResult,RealResult。
    也是為了分離調(diào)用和執(zhí)行,還使用了生產(chǎn)者消費者模式,將調(diào)用轉(zhuǎn)化為請求對象放到ActivationQueue里,由SchedulerThread實例從ActivationQueue里不斷取出請求對象,并執(zhí)行。

    適用場景:

    這個設(shè)計模式非常復雜,是否合適要考慮問題的規(guī)模,只有大規(guī)模的問題才適合使用該模式。

    注意事項:

    因為這個設(shè)計模式非常復雜,故此我們在使用時,一定注意各個對象的方法由哪些線程調(diào)用。比如Proxy對象的方法可能被多個線程同時調(diào)用,而Servant對象被封閉在Scheduler線程里,只有SchedulerThread線程才會調(diào)用它的方法,故此它是線程安全的,而RealResult可能會被多個線程使用,但它是Immutable的,FutureResult可能被多個線程同時調(diào)用,它封裝了兩個字段,故此需要使用synchronized保護,并且是采用Guarded Suspension模式保護FutureResult。

    總結(jié)

    以上是生活随笔為你收集整理的Java 多线程设计模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。