寫下這篇文章隻為了回顧之前在實際工作中犯的一個極其二逼的錯誤,用我的經曆來提示後來者,諸位程式大神,大牛,小牛們看到此文笑笑即可,輕拍輕拍。。。
有這麼一個需求,我們的系統(後面簡稱:a系統)需要在背景執行一個報表導出任務,在這個任務的執行過程中需要通過corba調用其他系統(後面簡稱:b系統)的一個(也有可能是多個)接口去查詢報表,待結果傳回後,将這些結果寫入excel。這個需求是不是很簡單?套用網上一些futuretask或者線程池的例子一兩小時就能搞定這個需求。當時我也是這樣認為的,可誰想,這是一個巨大的坑….
用過corba的同學會知道,如同資料庫連接配接一樣,corba的連接配接數也是是有限的,如果一個接口調用的時間過長,就會長時間占用corba有限的連接配接數,當這種長時間的同步調用過多時就會造成整個系統corba調用的阻塞,進而造成系統停止響應。由于查詢操作很耗時,為了避免這種情況的發生,這個接口被設計成了一個異步接口。任務的執行流程就會是這樣:任務開始執行,接着調用這個接口并且通過corba向b系統訂閱一個事件,然後任務進入等待狀态,當b系統執行完成後,會向a系統發送一個事件告知執行的結果,任務收到事件後重新開始執行直到結束,如圖:
既然說到了事件,那麼很自然而然的就想到了使用回調的方式去響應事件,并且為了避免事件逾時(也就是長時間沒有接收到事件)導緻任務長時間等待,我還使用了一個定時的任務去檢查任務的狀态。是以我的程式看起來就像這樣:
ieventfuture.java
exportrpttask.java
由于做這個需求的關系,我開始閱讀一些關于java多線程程式設計的一下教程,在閱讀到關于閉鎖的内容時,我突然靈光一現,這玩意不正好可以代替我那個醜陋的使用循環來讓任務進入等待狀态的實作麼?然後我的程式就變成了這樣:
正在我為我使用高大上的閉鎖代替循環沾沾自喜的時候,測試大爺告訴我,任務經常莫名其妙的失敗,并且日志中沒有任何異常。開始,這讓我覺得很不可思議,因為我已經在call()方法處處理了所有的異常,任務失敗時至少也應該有個日志啥的吧。這個問題一直困擾着我,直到有一天分析日志我突然發現任務執行的工作線程(也就是call()方法所在的線程)和接收到事件後的回調并不是同一個線程。這就意味着在查詢到報表結果後,所有寫excel,分發結果等等的操作都是在事件回調的線程中執行的,那麼一旦這裡發生異常原來call()中的catch塊自然無法捕獲,然後異常就被莫名其妙的吞掉了。好吧,我承認我之前對線程池也就了解點皮毛,對多線程也僅僅是有個概念,想當然的認為線上程池中可以hold住任務的一切,包括響應這個任務在執行過程中建立的其他線程運作時發生的異常。而且更嚴重的是按照原來的實作,隻有當整個任務執行完成(包括寫完excel)後,才會釋放那個閉鎖,是以一旦事件回調發生異常,那麼整個任務都無法終止。線上程池中發生一個任務永遠無法終止的後果,你懂的。
痛定思痛,我決定重新梳理這個任務的流程。這個需求的難點就是在如何監聽并響應b系統給我們發送的事件,實際上,這是一個很經典的生産者–消費者問題,而阻塞隊列正好是解決這類問題的利器。重新設計的事件響應流程就變成:當b系統發送事件的時候,事件回調線程會往阻塞隊列裡面填充一個事件。在另一方面,任務調用完b系統的查詢接口後,就開始從阻塞隊列中取事件,當事件隊列為空的時候,取事件的線程(也就是線程池執行任務的工作線程)會被阻塞。并且,阻塞隊列的取操作可以設定逾時時間,是以當取到的事件對象為空時,就意味着事件逾時了,這樣就省去了使用定時任務定時檢查任務狀态的工作。重新設計的程式是這樣的:
eventproxy.java
相信各位并發程式設計的大牛們能在一瞬間就可以把我的程式(包括改進後的)批得體無完膚,不過我還是想分享下我在這個過程中的收獲。
在動手寫程式前,請先了解你的需求,特别是要注意用已有的模型去識别問題,在本例中,我就是沒有識别響應事件的流程其實是個生産者–消費者問題導緻了後面的錯誤
請充分的了解你需要使用的技術和工具。比如,使用線程池你就要了解線程池的工作原理,這樣你才能正确的使用這些技術。做技術切忌想當然。
在使用線程池時,重要的操作盡量放在任務的主線程中執行(也就是call()/run()方法所在的線程),否則線程池本身難以對任務進行控制。
如果一定要在任務中再建立新的線程,請確定任務主線程是任務最後退出的線程。切忌不要使用外部線程直接調用任務類的方法,在本例中我就犯了這樣的錯誤。