摘要: 在軟件開發中,我們經常面臨著處理長時間任務的多線程編程問題。在我們的ezOne平台的開發中就多處涉及到,如JPC數據服務JPC數據處理服務 報警聯動 門禁系統等。本人在編寫DEMO程序的過程中幾易其稿,煞費心機,但依然感覺有許多地方需要改進,為了減少多線程編程帶來的風險,我翻譯整理了一個類似問題的解決方案框架以達到一勞永逸。 為了便於閱讀,保留原文。引用請保留作者和文章來源。
關鍵詞: Thread、 Lock 、 Notification、長時間任務。
在應用程序中我們經常需要一個類去完成像數據處理、監聽事件或檢查另一個類的活動等任務。為了達到這個目標,我們可能使用帶有一套鎖和消息通知的線程。JAVA 線程API已經很好的文檔化,但為了使線程能夠正確而高效地運行,程序員仍然需要豐富的編程經驗並編寫大量的代碼。通過應用本篇文章中討論的框架,程序員能夠避免忍受煎熬寫大量的代碼,快速創建健壯的應用程序。 二、長時間運行任務的程序框架。
關於長時間運行的任務的主要事情是如何在應用程序的生命期使它一直保持運行。實現的恰當方法是提供一個線程來執行這個特定的任務。我們可以通過繼承Thread類或實現java.lang.Runnable接口來達到該目標。如果采用實現Runnable接口的方式,就可以能夠獲得更好的面向對象的設計,同時可以避免JAVA中的單繼承問題。另外,我們也能更有效的處理Runnable實例(例如使用線程池通常需要一個Runnable實例而不是線程來運行)。
框架的基礎是一個叫Worker的抽象類,它實現了Runnable接口,並提供了有效處理任務的好方法。這些方法有些已經被實現,如run()方法,但有些是抽象方法,開發人員必須自己來實現。如果要創建一個長時間運行的類,你只需要繼承Worker類並實現幾個抽象方法。讓我們看看這些方法的細節。
Worker 類的run()方法被設計成只要不停止運行就持續的執行work()方法。work()方法可以負責數據處理、事件響應、文件讀寫、,執行SQL命令等操作。這樣work()方法能夠拋出異常,並將異常傳給run(),然後由run()方法來處理這些異常。
run()方法有內外兩層try-catch語句:一層處於while-loop循環外,一層在while-loop循環內。前一個try-catch用於捕獲非編程異常以確保run()方法不退出。後一個try-catch語句捕獲關於業務邏輯和相應行為的各種異常。如果在work()方法中發生了一些等待操作(例如等待一個輸入流或一個Socket),拋出一個InterruptedException的方法是可取的。要記住的是只要應用程序在運行,work()方法不需要任何while-loop循環去維持它運行,這一切由Worker代辦了。
run()開始時,調用prepareWorker()方法來准備長時間運行任務需要的所有資源(參考程序清單A)。例如 ,在這個方法中可以打開一個將要用到的數據庫連接或文件。尤其對於那些像建立一個socket這樣的阻塞操作放在這兒是很好的。因為若讓它們在一個獨立的線程中運行,則不會阻塞主線程的執行。
與前面方法相反的是releaseWorker(),它在run()方法准備退出時被調用(參考程序清單A)。在該方法中你可以編寫那些釋放系統資源或執行其它清除動作的代碼。該方法類似於java.lang.Object.finalize(),但它在線程中止時被顯式的調用。
三、框架中的錯誤處理機制
另一個重要的方法是handleError(),它帶有一個java.lang.Throwable的輸入參數。在run()方法每次發生錯誤時調用這個方法。這依賴於你怎麼實現錯誤處理。方法之一是寫錯誤日志並通過調用halt()方法中止任務(參考程序清單A)。
isCondition()方法用於判斷work()方法是否能夠被執行。因此允許細粒度地控制任務。這在事件觸發的框架中非常有用。當work()方法的執行條件未滿足時,work方法將被掛起,直到條件完全滿足(例如,緩存區非空)。在Worker的實現中這個條件將按在方法setTimeout()中指定的時間周期地檢查一個鎖通知。如果在任務中不需要任何等待阻塞,僅僅只要使isCondition()方法總是返回真值。
四、任務終止時機
你還需要isRunning(), broadcast(), halt()方法。通過訪問isRunning()方法,你將能檢查某個任務是否正在運行,並決定是否中止它。broadcast()方法正確地通知鎖對象,並且如果這個對象一直等待這個鎖,那麼就激活這個任務。halt()方法中止一個任務,因此下一isRunning()狀態一旦被調用,run()方法就退出,因為這個方法只通知那個可能阻塞這個任務線程的鎖。當在work()方法中執行阻塞作業時用相同的鎖是明智的。如果你不能用相同的鎖對象時,例如在執行java.io.InputStream.read()方法遇到阻塞時,你就應該添加所有可能鎖的顯式通知或者增加java.lang.Thread.interrupt()到halt()中。如果一個你阻塞的對象被正確處理,java.lang.Thread.interrupt()將會起作用。例如,它在InputStream.read()執行時有作用,但在執行java.sql.PreparedStatement.execute()不起作用,因此在每個特殊的條件下你必須測試halt()方法。
一旦你熟悉Worker類,你就很容易創建你自己的實現(參考程序清單B),為了把這類當作一個線程運行,僅僅只需簡單地使用 new Thread(new WaitedWorker()).start。應用Thread.interrupt()或Worker.halt()或它們的組合,你就可以准確的控制任務的執行。例如當JVM通過在java.lang.Runtime.addShutdownHook()方法中放相應的代碼停止時,你就能停止所有的任務。
五、結論
我們已經檢查了長時間運行任務框架,並且看到怎樣通過從創建一個基於它的抽象類的任務。它的構架是清晰和靈活的,並且被設計成可擴展的。用這個框架你能避免為創作類而絞盡腦汁,並且幫助你能夠開發出高效、可靠的應用程序。