前言 在最近一個月的面向對象編程學習中,我們進入了編寫多線程程式的階段。線程的創建、調度和信息傳遞,共用對象的處理,線程安全類的編寫,各種有關於線程的操作在一定程度上增加了近三次作業的複雜度與難度,帶來了不小的考驗。本文通過分析總結近三次作業的完成情況,分享我對與多線程編程的一些見解與體會。 作業總 ...
前言
在最近一個月的面向對象編程學習中,我們進入了編寫多線程程式的階段。線程的創建、調度和信息傳遞,共用對象的處理,線程安全類的編寫,各種有關於線程的操作在一定程度上增加了近三次作業的複雜度與難度,帶來了不小的考驗。本文通過分析總結近三次作業的完成情況,分享我對與多線程編程的一些見解與體會。
作業總結分析
多線程電梯調度
(1)題目簡述
實現具有捎帶功能的電梯調度系統,調度電梯數量為3部。
(2)程式設計
- 本系統的大致結構與之前的單線程電梯調度系統類似,主要由輸入處理、請求調度、電梯模擬三大部分組成。
- 根據行為的併發特征,為這三大部分各設計了一類線程來實現其功能。分別為輸入監聽線程、請求調度線程、電梯線程。
- 系統中的共用資源主要是請求隊列,根據請求隊列的類型分成了輸入隊列與執行隊列。二者繼承了請求集合類,並將這兩個類編寫成線程安全類。
- 根據生產者-消費者模型構造線程交互方法。
- 具體類圖如下:
-
時序圖:
(3)程式分析
- 複雜度度量:
- 分析:
- 在存儲請求隊列時採用了數組的數據結構。進行請求隊列的增減操作時需要遍曆數組,導致了相關方法迴圈複雜度較高。由於運用了長度變數控制遍歷的邊界,這些方法不會出現危險。可以考慮使用集合類管理此類數據,以減小代碼的複雜性,增加可維護性。
- 線程類的run方法過於複雜,使得該方法的複雜度異常的高。出現該問題主要由於對於線程類的功能的理解不夠到位。應當將線程的行為細化,並根據線程運行時需要執行的不同操作編寫相應的方法以供線程調用。這樣能提高線程類的可維護性。
- 數據統計:
(4)問題分析
本次作業的問題主要出現在以下兩個方面:
- 捎帶請求處理:本次作業沒有採用前幾次作業處理捎帶請求的方法,而是根據電梯的運行將分配到的請求按照到達先後順序構造隊列,電梯按照隊列頭改變運行狀態即可。但是在將新請求插入隊列的部分,邊界判斷出現了錯誤,導致當電梯停止時新請求的插入位置出錯。最終使電梯運行路線出錯。
- 輸出格式:對於不合法的輸入,輸出時的格式不對。更改輸出格式可以改正。
本次作業使第一次編寫多線程程式。由於對於每個線程的運行過程的理解不足,我在最初的程式構架時遇到了比較大的困難。尤其是在共用類中鎖的運用以及線程類的run方法的功能的設計上,經歷了多次刪除重寫的過程。在一定程度上,這導致了我在類的設計、調度演算法設計等方面有了不少疏忽。最終使得本次作業的完成效果一般。此外,線上程調度方面,本次作業較好的運用了課內講的模型。通過這次的鍛煉,我對於多線程編程的大體框架有了比較深刻的認識,為接下來的學習打下了鋪墊。
IFTTT
(1)題目簡述
實現IF-THIS-THEN-THAT的文件監控與操作系統。
(2)程式設計
-
經過分析,程式大致分為文件快照獲取、監控事件觸發、監控事件任務執行三部分。
- 根據行為的併發特征,為每個監控事件開啟一個線程,實現監控功能;並且設計一個負責實時輸出的線程。
-
具體類圖如下:
-
時序圖:
(3)程式分析
-
數據統計:
-
複雜度分析:
-
分析:
-
在構造文件系統的遍歷樹是採用了一維的線性數據結構,每次獲取快照時都需要按深度遞歸遍歷監控範圍內的所有文件,導致獲取快照的相關方法的複雜度較高。在優化的方面,可以考慮改用集合類中的樹結構或者哈希表的方式對監控範圍內的文件進行預處理(如編碼),減小之後的工作量。此外還可以直接使用現有庫中的獲取快照的方法。
- 本次作業對與線程類中的run方法進行了一定的精簡。相較於上次作業,複雜度有了一定的降低,但仍然是所有方法中最高的。可能的原因是我在最初設計時為單一線程分配了過多的功能。就本次作業而言,可以按照觸發器的不同將監控線程進行分化。而且還可以將快照獲取功能分離成單一線程,通過一個線程安全類與監控線程傳遞信息。
-
(4)問題分析
本次作業的問題主要出現在文件地址的管理方面。在程式中,每個文件都使用的是絕對地址格式。絕對地址有多種可識別格式,但通過File類提供的方法從File對象中獲取的絕對地址僅有一種格式。由於在前後快照對比是通過字元串比較實現的,所以可能由於格式問題導致錯誤。將格式同一後可以解決。
在本次作業中最重要的部分就是文件信息的同步。由於文件操作是線程不安全的,所以在完成作業的過程中,我用了相當多的時間在設計線程安全類和快照獲取功能上,最終獲得了不錯的效果。但在類功能的設計上還顯得有些冗雜,有挺大的改進空間。
模擬計程車打車系統
(1)題目簡述
實現100輛計程車的搶單調度系統。
(2)程式設計
-
- 乘客交互的時間特征:不定時產生乘客請求;請求產生3s後返回處理結果。
- 計程車交互數據特征:獲取計程車的位置信息、服務狀態和信用信息;返回其搶單結果和需要服務的乘客位置信息與目的地位置。
- 計程車交互時間特征:以100ms為周期獲取。
-
-
乘客的請求:
- 監聽獲取乘客發出的請求。
- 維護接受的請求集合。
-
乘客請求的響應視窗:
- 根據請求的位置與時間建立相應的搶單視窗。
- 維護相應視窗集合。
-
計程車:
-
維護計程車信息的更新。
-
維護計程車集合。
-
-
-
-
系統與乘客之間交互相對獨立,由於只由控制台輸入,使用一個線程設計用於與控制台輸入交互。
-
計程車的行為具有顯著的重覆模式,並且相對獨立,使用一種線程設計,分別描述每一輛計程車行為。
-
對於獲取的乘客請求的處理與分配具有重覆性,並且相對獨立,使用一種線程設計來實現功能。
-
信息輸出相對獨立,使用一個線程實現。
-
-
具體類圖如下:
-
時序圖:
(3)程式分析
-
數據統計:
-
複雜度分析:
-
分析:
- 相較於上次作業,本次作業方法的複雜度有了明顯的降低(不考慮現成的GUI代碼)。在最初設計的時候,我就考慮到了SOLID等原則,並儘量使設計的類符合以上的原則,做出了風格上的改變。其中線程類的設計上,儘量分配更少、更簡潔的功能,使其僅需完成線程部分必要的代碼。從而使得整個程式的可維護性與可讀性有了相當大的提高。
(4)問題分析
在測試階段我的程式沒有被髮現問題。但是在自己檢查的時候發現了不少的潛在問題:
- 地圖管理:本次作業我使用了gui中給好的地圖管理類,使用矩陣管理地圖。每當計程車接單時都需要調用廣度遍歷方法來獲取最短路徑。這使得計程車在路徑的選擇上缺乏功能延展性,只能走最初設定好的路線,無法適應路況的變化。
-
計程車管理:在本次作業中,我為了簡單將所有的計程車都管理在了同一個數組中,將計程車狀態保存在了計程車各自的對象中。如果能將不同狀態的計程車分離,用不同的策略去管理,則能夠使程式的延展性更高。
總結
經過了這三次的多線程編程作業,通過分析作業中的問題,我對於線程的調度與線程的安全有了相當深入的認識。從巨集觀上來看,每個線程都在同時運行,但微觀上看他們是在JVM的調度之下按原子操作交替執行。如果每個線程間相互獨立,JVM調度的不確定性不會影響程式的可再現性。但就像電梯中的請求隊列、IFTTT中的文件信息、計程車系統的請求和計程車信息,每個線程間難以避免地會有共用的數據,此時就需要通過同步、互斥的手段防止JVM調度的不確定性破壞程式的可再現性。通常,通過鎖機制就能夠實現這些功能。但是,在共用關係比較複雜的情況下,單純的使用鎖機制並不一定能夠達到預期的效果。這時就需要一種模式化的線程安全保護措施。那就是線程安全類。編寫線程安全類就好比為已有的功能代碼加上一層外皮,其內部代碼保證功能的實現,外部介面保證入口的互斥,從而實現線程安全。但這又帶來了另一個問題:同步部分的代碼長度對多線程效率的影響。進而對於臨界區的功能安排應當儘量精簡,以避免對多線程機制的浪費。
此外,經過了對面向對象思想的學習,我認識到了面向對象程式設計的12大基本原則,並且將其實踐到最近的一次作業當中。在親自編寫代碼的過程中,我體會到這些原則最核心的想法就是:設計具有層次性、代碼具有可延展性。在設計時就要從最外層的交互設計,一層層深入,到內部對象的建模、對象間交互,再到類內部的設計。這個設計就是對整個環境與系統的逐層深入,將各個功能逐層分離,最終形成類似樹狀的類設計。而在編寫代碼的時候不應當僅關註於當前的功能實現,更應當想到更多同類型的操作。換句話說就是編寫出的方法、類不應當進能夠實現特例操作,而更應當面向更為抽象、更為通用的層次上。