天天看點

線程池有界隊列和無界隊列_線程池和工作隊列

學到更多。 開發更多。 連接配接更多。

新的developerWorks Premium會員計劃可通過Safari圖書線上獲得對強大的開發工具和資源的無障礙通路權,其中包括500個頂級技術标題(數十個專門針對Java開發人員),主要開發人員活動的超低折扣,最近O'Reilly的視訊重播會議等。 立即注冊 。

為什麼要使用線程池?

許多伺服器應用程式,例如Web伺服器,資料庫伺服器,檔案伺服器或郵件伺服器,都是圍繞處理大量來自某個遠端源的簡短任務而設計的。 請求以某種方式到達伺服器,該方式可能是通過網絡協定(例如HTTP,FTP或POP),通過JMS隊列,或者可能是通過輪詢資料庫。 不管請求如何到達,在伺服器應用程式中,通常每個任務的處理都是短暫的,并且請求的數量很大。

用于建構伺服器應用程式的一種簡化模型是,每次請求到達時建立一個新線程,并在新線程中處理該請求。 這種方法實際上可以很好地用于原型制作,但是有很多明顯的缺點,如果您嘗試部署以這種方式工作的伺服器應用程式,這些缺點将變得顯而易見。 每個請求線程方法的缺點之一是為每個請求建立一個新線程的開銷很大。 如果伺服器為每個請求建立一個新線程,則與處理實際使用者請求相比,它會花費更多的時間并花費更多的系統資源來建立和銷毀線程。

除了建立和銷毀線程的開銷外,活動線程還會消耗系統資源。 由于過多的記憶體消耗,在一個JVM中建立太多線程可能會導緻系統記憶體不足或崩潰。 為了防止資源崩潰,伺服器應用程式需要某種方式來限制在任何給定時間正在處理多少個請求。

線程池為線程生命周期開銷問題和資源崩潰問題提供了解決方案。 通過将線程重用于多個任務,線程建立開銷分散在許多任務上。 另外,由于在請求到達時線程已經存在,是以消除了線程建立帶來的延遲。 是以,可以立即為請求提供服務,進而使應用程式更具響應性。 此外,通過适當地調整線程池中的線程數,可以通過強制超過某個門檻值的任何請求等待直到有線程可以處理它來防止資源崩潰。

線程池的替代方法

線程池不是在伺服器應用程式中使用多個線程的唯一方法。 如上所述,有時候為每個新任務生成一個新線程是完全明智的。 但是,如果任務建立的頻率很高且平均任務持續時間很短,則為每個任務生成新線程将導緻性能問題。

另一個常見的線程模型是具有單個背景線程和用于某種類型任務的任務隊列。 AWT和Swing使用此模型,其中有一個GUI事件線程,并且導緻使用者界面更改的所有工作都必須在該線程中執行。 但是,由于隻有一個AWT線程,是以不希望在AWT線程中執行可能要花費明顯時間才能完成的任務。 結果,Swing應用程式通常需要其他工作線程來長時間運作與UI相關的任務。

每個任務線程和單背景線程方法在某些情況下都可以很好地工作。 “每任務線程”方法與少量長時間運作的任務配合得很好。 隻要排程的可預測性不重要,那麼單背景線程方法就可以很好地工作,低優先級背景任務就是這種情況。 但是,大多數伺服器應用程式以處理大量短期任務或子任務為中心,是以,希望有一種機制能夠以低開銷有效地處理這些任務,以及某種程度的資源管理和時間可預測性。 線程池具有這些優點。

工作隊列

就線程池的實際實作方式而言,術語“線程池”在某種程度上具有誤導性,因為在大多數情況下,線程池的“顯而易見”的實作不能完全産生我們想要的結果。 術語“線程池”早于Java平台,并且可能是來自不太面向對象的方法的産物。 盡管如此,該術語仍被廣泛使用。

雖然我們可以輕松實作線程池類,其中用戶端類将等待可用線程,将任務傳遞給該線程以執行,然後在完成時将線程傳回到池中,但這種方法可能會産生一些不良影響。 例如,當池為空時會發生什麼? 任何嘗試将任務傳遞給池線程的調用方都将發現該池為空,并且其線程在等待可用池線程時将被阻塞。 通常,我們希望使用背景線程的原因之一是防止送出線程被阻塞。 像線上程池的“顯而易見的”實作中那樣,将阻塞一直推到調用者,最終可能會導緻我們試圖解決的相同問題。

我們通常想要的是一個工作隊列,該隊列與一組固定的工作線程組合在一起,該工作線程使用

wait()

notify()

來向等待線程發出新工作到達的信号。 通常将工作隊列實作為帶有關聯監視對象的某種連結清單。 清單1顯示了一個簡單的合并工作隊列的示例。 這種模式使用

Runnable

對象隊列,是排程程式和工作隊列的通用約定,盡管Thread API并沒有特别要求使用

Runnable

接口。

public class WorkQueue
{
    private final int nThreads;
    private final PoolWorker[] threads;
    private final LinkedList queue;

    public WorkQueue(int nThreads)
    {
        this.nThreads = nThreads;
        queue = new LinkedList();
        threads = new PoolWorker[nThreads];

        for (int i=0; i<nThreads; i++) {
            threads[i] = new PoolWorker();
            threads[i].start();
        }
    }

    public void execute(Runnable r) {
        synchronized(queue) {
            queue.addLast(r);
            queue.notify();
        }
    }

    private class PoolWorker extends Thread {
        public void run() {
            Runnable r;

            while (true) {
                synchronized(queue) {
                    while (queue.isEmpty()) {
                        try
                        {
                            queue.wait();
                        }
                        catch (InterruptedException ignored)
                        {
                        }
                    }

                    r = (Runnable) queue.removeFirst();
                }

                // If we don't catch RuntimeException, 
                // the pool could leak threads
                try {
                    r.run();
                }
                catch (RuntimeException e) {
                    // You might want to log something here
                }
            }
        }
    }
}
           

您可能已經注意到,清單1中的實作使用

notify()

而不是

notifyAll()

。 大多數專家建議使用

notifyAll()

而不是

notify()

,這有充分的理由:使用

notify()

存在一些細微的風險,并且僅在特定的特定條件下使用才是合适的。 另一方面,當正确使用時,

notify()

具有比

notifyAll()

更理想的性能特征; 特别是,

notify()

導緻更少的上下文切換,這在伺服器應用程式中很重要。

清單1中的示例工作隊列滿足安全使用

notify()

的要求。 是以,請繼續在程式中使用它,但是在其他情況下使用

notify()

時要

notify()

小心。

使用線程池的風險

盡管線程池是用于構造多線程應用程式的強大機制,但它并非沒有風險。 使用線程池建構的應用程式與任何其他多線程應用程式一樣,都面臨所有相同的并發風險,例如同步錯誤和死鎖,以及線程池特有的其他一些風險,例如與池相關的死鎖,資源颠簸和線程洩漏。

僵局

對于任何多線程應用程式,都有死鎖的風險。 一組程序或線程在每個程序或線程正在等待事件中的死鎖時,隻有該組中的另一個程序可以引起該事件。 死鎖最簡單的情況是,線程A持有對象X的排他鎖并等待對象Y的鎖,而線程B持有對象Y的排他鎖并等待對象X的鎖。為了擺脫等待鎖(Java鎖不支援)的方式,死鎖的線程将永遠等待。

盡管死鎖是任何多線程程式中的風險,但是線程池為死鎖帶來了另一種機會,其中所有池線程正在執行被阻塞的任務,等待隊列中另一個任務的結果,但是另一個任務無法運作,因為沒有空閑的空間。線程可用。 當使用線程池來實作涉及許多互動對象的模拟時,就會發生這種情況,并且模拟的對象可以向彼此發送查詢,然後将它們作為排隊的任務執行,并且查詢對象同步等待響應。

資源th動

線程池的一個好處是,相對于替代的排程機制,線程池通常表現良好,我們已經讨論了其中的一些機制。 但這隻有在正确調整線程池大小的情況下才是正确的。 線程消耗大量資源,包括記憶體和其他系統資源。 除了

Thread

對象所需的記憶體外,每個線程還需要兩個執行調用堆棧,這可能會很大。 此外,JVM可能會為每個Java線程建立一個本機線程,這将消耗額外的系統資源。 最後,盡管線程之間切換的排程開銷很小,但是對于許多線程而言,上下文切換可能會嚴重拖累程式的性能。

如果線程池太大,則這些線程消耗的資源可能會對系統性能産生重大影響。 線上程之間切換會浪費時間,并且線程數量超過您的需求可能會導緻資源匮乏問題,因為池線程正在消耗資源,其他任務可以更有效地利用這些資源。 除了線程本身使用的資源之外,完成服務請求的工作可能還需要其他資源,例如JDBC連接配接,套接字或檔案。 這些資源也是有限的,并且并發請求過多可能會導緻失敗,例如無法配置設定JDBC連接配接。

并發錯誤

線程池和其他排隊機制依賴使用

wait()

notify()

方法,這可能很棘手。 如果編碼不正确,則可能會丢失通知,進而導緻線程保持空閑狀态,即使隊列中有待處理的工作也是如此。 使用這些設施時必須格外小心; 甚至專家也會犯錯。 更好的是,使用已知有效的現有實作,例如下面無需編寫自己的中讨論的

util.concurrent

包。

螺紋洩漏

在所有類型的線程池中,重大的風險是線程洩漏,這種情況發生在從線程池中删除線程以執行任務時,但在任務完成時并未将其傳回池中。 一種發生這種情況的方式是,當任務抛出

RuntimeException

Error

。 如果池類沒有捕獲到這些,則線程将簡單退出,線程池的大小将永久減少一。 如果發生這種情況足夠多的時間,則線程池最終将為空,并且由于沒有線程可用于處理任務,系統将停止運作。

永久停止的任務(例如,可能永遠等待無法保證可用的資源或可能已回家的使用者的輸入的任務)也可能導緻線程洩漏。 如果線程被此類任務永久占用,則實際上已将其從池中删除。 應該為此類任務配置設定自己的線程,或者僅等待有限的時間。

請求超載

伺服器有可能隻是被請求所淹沒。 在這種情況下,我們可能不想将每個傳入請求都排隊到我們的工作隊列中,因為排隊等待執行的任務可能會消耗過多的系統資源并導緻資源匮乏。 在這種情況下,您應自行決定要做什麼; 在某些情況下,您可以簡單地将請求丢棄,依靠更進階别的協定在以後重試該請求,或者您可能希望以訓示伺服器暫時繁忙的響應來拒絕該請求。

有效使用線程池的準則

隻要遵循一些簡單的準則,線程池就可以成為建構伺服器應用程式的一種非常有效的方法:

  • 不要将同步等待其他任務結果的任務排隊。 這可能會導緻上述形式的死鎖,在該死鎖中,所有線程都被任務占用,而這些任務又輪流等待由于所有線程都忙而無法執行的排隊任務的結果。
  • 在将池化線程用于可能長期運作的操作時要小心。 如果程式必須等待資源(例如I / O完成),請指定最大等待時間,然後使任務失敗或重新排隊以供以後執行。 這樣可以保證通過釋放線程來完成可能成功完成的任務,最終将取得一些進展。
  • 了解您的任務。 為了有效地調整線程池的大小,您需要了解正在排隊的任務以及它們在做什麼。 它們是否受CPU限制? 它們是否受I / O限制? 您的答案将影響您調整應用程式的方式。 如果您具有特性完全不同的不同類别的任務,則可以為不同類型的任務使用多個工作隊列,是以可以相應地調整每個池。

調整池大小

調整線程池的大小主要是要避免兩個錯誤:線程過多或線程過多。 幸運的是,對于大多數應用程式,太少和太多之間的中間地帶是相當寬的。

回想一下,在應用程式中使用線程有兩個主要優點:允許處理在等待諸如I / O之類的緩慢操作的同時繼續進行處理,以及利用多個處理器的可用性。 在運作于N處理器計算機上的受計算限制的應用程式中,随着線程數量接近N,添加其他線程可能會提高吞吐量,但是添加超過N的其他線程将無濟于事。 實際上,由于額外的上下文切換開銷,太多線程甚至會降低性能。

線程池的最佳大小取決于可用處理器的數量以及工作隊列上任務的性質。 在一個N處理器系統上,該隊列将完全容納受計算限制的任務,通常使用N或N + 1線程的線程池可以達到最大的CPU使用率。

對于可能等待I / O完成的任務(例如,從套接字讀取HTTP請求的任務),您将要增加池大小,使其超出可用處理器的數量,因為并非所有線程都在工作每時每刻。 使用概要分析,您可以估算典型請求的等待時間(WT)與服務時間(ST)的比率。 如果我們将此比率稱為WT / ST,則對于N處理器系統,您将需要大約N *(1 + WT / ST)個線程來保持處理器的充分利用。

處理器使用率不是調整線程池大小的唯一考慮因素。 随着線程池的增長,您可能會遇到排程程式,可用記憶體或其他系統資源的限制,例如套接字數,打開的檔案句柄或資料庫連接配接。

不用自己寫

Doug Lea編寫了一個出色的并發實用程式開源庫

util.concurrent

,其中包括互斥體,信号量,隊列和哈希表等收集類,這些類在并發通路下表現良好,并提供了幾種工作隊列實作。 此程式包中的

PooledExecutor

類是基于工作隊列的高效,廣泛使用的線程池的正确實作。 您可以考慮使用

util.concurrent

某些實用程式,而不是嘗試自己編寫,這很容易出錯。

util.concurrent

庫也是JSR 166(一個Java社群流程(JCP)工作組)的靈感來源,該工作組将産生一組并發實用程式,以包含在

java.util.concurrent

包下的Java類庫

java.util.concurrent

,并且應該已經為Java Development Kit 1.5發行版做好了準備。

結論

線程池是用于組織伺服器應用程式的有用工具。 它在概念上很簡單,但是在實作和使用它時要注意幾個問題,例如死鎖,資源颠簸以及

wait()

notify()

的複雜性。 如果發現自己的應用程式需要線程池,請考慮使用

util.concurrent

中的

Executor

類之一,例如

PooledExecutor

,而不是從頭開始編寫。 如果您發現自己正在建立線程來處理短期任務,則絕對應該考慮使用線程池。

翻譯自: https://www.ibm.com/developerworks/java/library/j-jtp0730/index.html