多線程-組合整理

線程:

關於生命週期的五種狀態是要清楚的,知道線程的生命週期很重要, new->runnable->running->blocked->dead

新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就     緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

阻塞最重要,消息類處理,深入理解阻塞,至關重要。

阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

線程創建:

老一點的兩種方式,用extend Thread Or implements Runnable,Runable也是集成Thread的,線程需要處理的業務就在public void run()裏實現。

最後啓動,需要注意的是,runnable還是要給Thread來調起,如下代碼:

Thread:

Thread1 t1 = new Thread1("t1");
Thread1 t2 = new Thread1("t2");
t1.start();
t2.start();

Runnable:

RunnableTest r1 = new RunnableTest("t1");
Thread t1 = new Thread(r1);
RunnableTest r2 = new RunnableTest("t2");
Thread t2 = new Thread(r2);
t1.start();
t2.start();

現在有用一種新的方式了 Callable和Future,在這裏一起學習下

Callable接口類似於Runnable,但是Runnable裏執行任務比較封閉,不會返回線程結果,並且無法拋出返回結果的異常,而Callable功能更強大一些,被線程執行後,可以返回值,這個返回值可以被Future拿到,也就是說,Future可以拿到異步執行任務的返回值。

執行結果:加上時間以後,比較清楚了,程序的最後,是等線程裏結束了,全部結束。 會節約很多時間。

FutureTask實現了兩個接口,Runnable和Future,所以它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值,那麼這個組合的使用有什麼好處呢?假設有一個很耗時的返回值需要計算,並且這個返回值不是立刻需要的話,那麼就可以使用這個組合,用另一個線程去計算返回值,而當前線程在使用這個返回值之前可以做其它的操作,等到需要這個返回值時,再通過Future得到,豈不美哉!這裏有一個Future模式的介紹:http://openhome.cc/Gossip/DesignPattern/FuturePattern.htm。 

下面來看另一種方式使用Callable和Future,通過ExecutorService的submit方法執行Callable,並返回Future,代碼如下

執行結果

代碼是不是簡化了很多,不用new Thread了,ExecutorService繼承自Executor,它的目的是爲我們管理Thread對象,從而簡化併發編程,Executor使我們無需顯示的去管理線程的生命週期,是JDK 5之後啓動任務的首選方式。

執行多個帶返回值的任務,並取得多個返回值,代碼如下:

其實也可以不使用CompletionService,可以先創建一個裝Future類型的集合,用Executor提交的任務返回值添加到集合中,最後遍歷集合取出數據,代碼略。更新於2016-02-05,評論中就這個說法引發了討論,其實是我沒有講清楚,抱歉。這裏再闡述一下:提交到CompletionService中的Future是按照完成的順序排列的,這種做法中Future是按照添加的順序排列的。所以這兩種方式的區別就像評論中fishjam所描述的那樣。

轉自博客http://blog.csdn.net/ghsau/article/details/7451464

-------------------------------------------------

使用Callable和Future接口創建線程。具體是創建Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作爲Thread對象的target來創建線程。

callable和future應該還有多種形式,這個以後用到再具體研究吧。

創建方式說完,繼續回到線程阻塞吧,這是一個大塊,需要好好理解

--------------------以下是線程阻塞-----------------------

http://blog.csdn.net/seelye/article/details/7656934

    阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒),學過操作系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支持阻塞,下面讓我們逐一分析。

 1. sleep() 方法:sleep() 允許 指定以毫秒爲單位的一段時間作爲參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,線程重新進入可執行狀態。

    典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足爲止。

 2. suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume() 使其恢復。

 3. yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,但是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間從而轉到另一個線程。

 4. wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒爲單位的一段時間作爲參數,另一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態,後者則必須對應的 notify() 被調用。

    初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。wait()會釋放對象鎖

    上述的核心區別導致了一系列的細節上的區別。

    首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因爲這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用 任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

    其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized 方法或塊中當前線程才佔有鎖,纔有鎖可以釋放。同樣的道理,調用這一對方法的對象上的鎖必須爲當前線程所擁有,這樣纔有鎖可以釋放。因此,這一對方法調用必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。

    wait() 和 notify() 方法的上述特性決定了它們經常和synchronized 方法或塊一起使用,將它們和操作系統的進程間通信機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於操作系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均聲明爲 synchronized)。它們的結合使得我們可以實現操作系統上一系列精妙的進程間通信的算法(如信號量算法),並用於解決各種複雜的線程間通信問題。

    關於 wait() 和 notify() 方法最後再說明兩點:

    第一:調用 notify() 方法解除阻塞的線程,是從因調用該對象的 wait() 方法的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要特別小心,避免因這種不確定性而產生問題。

    第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。

    談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的調用都可能產生死鎖。遺憾的是,Java 並不在語言級別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。

    以上我們對 Java 中實現線程阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify() 方法,因爲它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。

-----------------------------------------------

http://blog.csdn.net/hll814/article/details/50816268

多線程的應用:

通俗的解釋一下多線程先:

多線程用於堆積處理,就像一個大土堆,一個推土機很慢,那麼10個推土機一起來處理,當然速度就快了,不過由於位置的限制,如果20個推土機,那麼推土機之間會產生相互的避讓,相互摩擦,相互擁擠,反而不如10個處理的好,所以,多線程處理,線程數要開的恰當,就可以提高效率。

多線程使用的目的:

1、  吞吐量:做WEB,容器幫你做了多線程,但是它只能幫你做請求層面的,簡單的說,就是一個請求一個線程(如struts2,是多線程的,每個客戶端請求創建一個實例,保證線程安全),或多個請求一個線程,如果是單線程,那隻能是處理一個用戶的請求

2、  伸縮性:通過增加CPU核數來提升性能。

多線程在程序中的作用:

1、提高前端請求的響應速度。當我們執行一個比較耗時的方法時,http請求得不到響應甚至會超時,這時如果業務上允許數據的延遲,我們可以使用多線程來進行處理比較耗時的方法。這樣前端發送了請求,後端令開啓了一個線程去處理任務,就不會阻塞主線程了。

2、減清服務器的壓力。包括我們的web容器,如tomcat、jetty等,還有數據庫服務器等。因爲我們使用了多線程,並且線程池大小有限制,如30,那麼同時請求數據庫的鏈接就限制爲30了,也就是說能夠同時執行方法的線程只有30個,其餘的任務都放在我們線程的任務隊列了,這樣數據庫就不會被突然上來的請求給壓垮了。當然對於緩解數據庫壓力來說,更建議使用消息隊列,使用消息隊列我們可以攢數據進行批量提交,而僅僅使用多線程,則不好實現攢數據的過程。

3、提高處理能力,增加性能,充分利用服務器資源。如我們要將三個表裏的數據加載到緩存,最簡單的我們一個表開啓一個線程,共用三個線程去加載緩存則比用一個線程去挨着遍歷三個表的數據高效的多。

多線程的使場景:

1、  常見的瀏覽器、Web服務(現在寫的web是中間件幫你完成了線程的控制),web處理請求,各種專用服務器(如遊戲服務器)

2、  servlet多線程,多連接

3、  FTP下載,多線程操作文件

4、  數據庫用到的多線程

5、  分佈式計算

6、  tomcat,tomcat內部採用多線程,上百個客戶端訪問同一個WEB應用,tomcat接入後就是把後續的處理扔給一個新的線程來處理,這個新的線程最後調用我們的servlet程序,比如doGet或者dpPost方法

7、  後臺任務:如定時向大量(100W以上)的用戶發送郵件;定期更新配置文件、任務調度(如quartz),一些監控用於定期信息採集

8、  自動作業處理:比如定期備份日誌、定期備份數據庫

9、  異步處理:如發微博、記錄日誌

10、 頁面異步處理:比如大批量數據的核對工作(有10萬個手機號碼,覈對哪些是已有用戶)

11、 數據庫的數據分析(待分析的數據太多),數據遷移

12、 多步驟的任務處理,可根據步驟特徵選用不同個數和特徵的線程來協作處理,多任務的分割,由一個主線程分割給多個線程完成

13、 desktop應用開發,一個費時的計算開個線程,前臺加個進度條顯示

14、 swing編程

舉一個小栗子:

 

一個文本文件有100M,全是字符串,我要執行切分字符串,每達到N長度便執行切腹,最後求切分完成的字符串的集合

單線程處理:讀取文本文件數據,掃描全部數據,一個一個的切分,最後消耗時間=文件傳輸時間(文本數據加載到內存)+切分過程消耗

多線程處理:

專門設置一個線程執行加載數據的操作,此時,如果加載的數據達到一個設定值,啓動一個切線程處理,如此繼續,多個切分字符串的線程能夠併發執行,CPU的利用率提高了(文件傳輸的過程中沒有佔用處理器,而可以將加載的部分數據分配給切分線程,佔用處理器來執行任務)

總結:

單線程處理,文件加載的過程中,處理器一直空閒,但也被加入到總執行時間之內,串行執行切分總時間,等於每切分一個時間*切分後字符串的個數,執行程序,估計等幾分鐘能處理完就不錯了

多線程處理,文件加載過程與拆分過程,拆分過程與拆分過程,都存在併發——文件加載的過程中就執行了切分任務,切分任務執行過程中多線程並行處理,總消耗時間能比單線程提高很多,甚至幾個數量級都不止。

------------應用場景下面這篇說的挺逗

https://www.cnblogs.com/kenshinobiy/p/4671314.html

多線程使用的主要目的在於:

1、吞吐量:你做WEB,容器幫你做了多線程,但是他只能幫你做請求層面的。簡單的說,可能就是一個請求一個線程。或多個請求一個線程。如果是單線程,那同時只能處理一個用戶的請求。

2、伸縮性:也就是說,你可以通過增加CPU核數來提升性能。如果是單線程,那程序執行到死也就利用了單核,肯定沒辦法通過增加CPU核數來提升性能。

鑑於你是做WEB的,第1點可能你幾乎不涉及。那這裏我就講第二點吧。

--舉個簡單的例子:
假設有個請求,這個請求服務端的處理需要執行3個很緩慢的IO操作(比如數據庫查詢或文件查詢),那麼正常的順序可能是(括號裏面代表執行時間):
a、讀取文件1  (10ms)
b、處理1的數據(1ms)
c、讀取文件2  (10ms)
d、處理2的數據(1ms)
e、讀取文件3  (10ms)
f、處理3的數據(1ms)
g、整合1、2、3的數據結果 (1ms)
單線程總共就需要34ms。
那如果你在這個請求內,把ab、cd、ef分別分給3個線程去做,就只需要12ms了。

所以多線程不是沒怎麼用,而是,你平常要善於發現一些可優化的點。然後評估方案是否應該使用。
假設還是上面那個相同的問題:但是每個步驟的執行時間不一樣了。
a、讀取文件1  (1ms)
b、處理1的數據(1ms)
c、讀取文件2  (1ms)
d、處理2的數據(1ms)
e、讀取文件3  (28ms)
f、處理3的數據(1ms)
g、整合1、2、3的數據結果 (1ms)
單線程總共就需要34ms。
如果還是按上面的劃分方案(上面方案和木桶原理一樣,耗時取決於最慢的那個線程的執行速度),在這個例子中是第三個線程,執行29ms。那麼最後這個請求耗時是30ms。比起不用單線程,就節省了4ms。但是有可能線程調度切換也要花費個1、2ms。因此,這個方案顯得優勢就不明顯了,還帶來程序複雜度提升。不太值得。

那麼現在優化的點,就不是第一個例子那樣的任務分割多線程完成。而是優化文件3的讀取速度。
可能是採用緩存和減少一些重複讀取。
首先,假設有一種情況,所有用戶都請求這個請求,那其實相當於所有用戶都需要讀取文件3。那你想想,100個人進行了這個請求,相當於你花在讀取這個文件上的時間就是28×100=2800ms了。那麼,如果你把文件緩存起來,那隻要第一個用戶的請求讀取了,第二個用戶不需要讀取了,從內存取是很快速的,可能1ms都不到。

僞代碼:

public class MyServlet extends Servlet{

    private static Map<String, String> fileName2Data = new HashMap<String, String>();

    private void processFile3(String fName){
        String data = fileName2Data.get(fName);
        if(data==null){
            data = readFromFile(fName);    //耗時28ms
            fileName2Data.put(fName, data);
        }
        //process with data
    }

}


看起來好像還不錯,建立一個文件名和文件數據的映射。如果讀取一個map中已經存在的數據,那麼就不不用讀取文件了。
可是問題在於,Servlet是併發,上面會導致一個很嚴重的問題,死循環。因爲,HashMap在併發修改的時候,可能是導致循環鏈表的構成!!!(具體你可以自行閱讀HashMap源碼)如果你沒接觸過多線程,可能到時候發現服務器沒請求也巨卡,也不知道什麼情況!
好的,那就用ConcurrentHashMap,正如他的名字一樣,他是一個線程安全的HashMap,這樣能輕鬆解決問題。

public class MyServlet extends Servlet{
    private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
    private void processFile3(String fName){
                String data = fileName2Data.get(fName);
                if(data==null){
                        data = readFromFile(fName);    //耗時28ms
                        fileName2Data.put(fName, data);
                    }
                //process with data
            }
}

這樣真的解決問題了嗎,這樣雖然只要有用戶訪問過文件a,那另一個用戶想訪問文件a,也會從fileName2Data中拿數據,然後也不會引起死循環。
可是,如果你覺得這樣就已經完了,那你把多線程也想的太簡單了,騷年!
你會發現,1000個用戶首次訪問同一個文件的時候,居然讀取了1000次文件(這是最極端的,可能只有幾百)。What the fuckin hell!!!
難道代碼錯了嗎,難道我就這樣過我的一生!
好好分析下。Servlet是多線程的,那麼

public class MyServlet extends Servlet{

    private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();

    private void processFile3(String fName){
        String data = fileName2Data.get(fName);
        //“偶然”-- 1000個線程同時到這裏,同時發現data爲null
        if(data==null){
            data = readFromFile(fName);    //耗時28ms
            fileName2Data.put(fName, data);
        }
        //process with data

    }

}

上面註釋的“偶然”,這是完全有可能的,因此,這樣做還是有問題。

因此,可以自己簡單的封裝一個任務來處理。

public class MyServlet extends Servlet{

    private static ConcurrentHashMap<String, FutureTask> fileName2Data = new ConcurrentHashMap<String, FutureTask>();
    private static ExecutorService exec = Executors.newCacheThreadPool();
    private void processFile3(String fName){
        FutureTask data = fileName2Data.get(fName);
        //“偶然”-- 1000個線程同時到這裏,同時發現data爲null
        if(data==null){
            data = newFutureTask(fName);
            FutureTask old = fileName2Data.putIfAbsent(fName, data);
            if(old==null){
              data = old;
            }else{
              exec.execute(data);
            }
        }
        String d = data.get();
        //process with data
    }

    private FutureTask newFutureTask(final String file){

        return  new FutureTask(new Callable<String>(){
            public String call(){
                return readFromFile(file);
            }
            private String readFromFile(String file){return "";}
        }
    }

}


以上所有代碼都是直接在bbs打出來的,不保證可以直接運行。

多線程最多的場景:web服務器本身;各種專用服務器(如遊戲服務器);
多線程的常見應用場景:
1、後臺任務,例如:定時向大量(100w以上)的用戶發送郵件;
2、異步處理,例如:發微博、記錄日誌等;
3、分佈式計算

--------------------------

線程安全:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章