2019年北航OO第二單元(多線程電梯任務)總結


一、三次作業總結

1. 說在前面

對於這次的這三次電梯作業,我采用了和幾乎所有人都不同的架構:將每個人當作一個線程。這樣做有一定的好處:它使得整個問題的建模更加自然,並且在后期人員調度變得復雜時,可以將調度器上紛繁的邏輯判斷分布在不同的人身上,大大簡化了代碼邏輯。對於程序復雜度,將人作為某個容器中的PersonRequest時需要在電梯到達某一層時進行遍歷,而將人作為線程池中的一個任務則是通過wait()notify()機制實現了類似的線程遍歷,對於此次最多40人的簡單任務而言並不會在時間上損失太多;在debug時,通過將每個人的線程進行重命名,我可以輕易地使用JProfiler等工具查看究竟是哪一個線程沒有正常結束,省去了部分調試輸出的麻煩。綜合了建模的簡化和時間上可接受的損失,我選擇了將每個人作為一個線程進行處理。

對於設計,我不希望將自己的設計僅僅局限於每次作業中課程組提出的設計目標,而是盡可能留出可以擴展的余地,這也是為何我從第五次作業開始就采用了和最后一次作業極為相近的設計結構,這樣可以使得我每次作業的代碼復用率盡可能高。實際上,在這幾次作業中我的調度器類Scheduler、輸入類InputHandler、幾個自己通過繼承和實現接口寫的數據結構都幾乎沒有改動,甚至任務的關鍵部分乘客類Person和電梯類Elevator的改動也很小且復用率很高。當能夠在更早的時候預想未來的可能需求,便能在盡可能最早的時間通過設計上的優化為未來做出准備。

2. 第五次作業

2.1 需求分析

本次作業是需要寫一個傻瓜調度電梯:一個電梯,每次只有一人乘坐,采用FCFS先來先服務策略。

2.2 實現方案

第五次作業是一個再顯然不過的單生產者-單消費者模型。在Java中,解決生產者-消費者模型的標准方法就是使用java.util.concurrent包中提供的阻塞隊列BlockingQueue。因此,本次作業基本上是圍繞着兩個線程共享的阻塞隊列LinkedBlockingQueue進行操作的。為了給后續作業留出余地,我從第五次作業中就加入了調度器Scheduler(盡管這在第五次作業中是不必要的,但是為了減輕后續debug的壓力,我選擇將這個潛在的易錯點從最簡單的第五次作業開始引入)。需要維護的阻塞隊列有兩個:輸入線程和調度器之間的隊列,以及調度器和電梯之間的隊列,而這兩個也正是兩個分別的生產者-消費者模型。

對於每個人線程Person,在第五次作業中同時只會有一個Person線程由電梯發起運行,所以不需要維護線程池。人會對所需要乘坐的電梯進行等待(即elevator.wait()),當電梯到達某一層開門后即notifyAll()喚醒等待該電梯的乘客。乘客在被喚醒后會判斷是否開門以及是否到達所需樓層,若滿足要求則進出電梯。

對於調度策略,電梯在此次作業中運行模式是最簡單的先來先服務,每次只需要考慮在運行的Person線程的單一需求即可。

對於同步策略,本次作業中通過使用兩個線程安全的BlockingQueue分別實現了兩個生產者-消費者模型的線程安全。對於人和電梯之間的交互則是通過對電梯加內部鎖synchronized(elevator)完成的。由於monitor只會在Person線程中,因此這種同步是沒有問題的。對於所有屬性,我都盡可能加了final標識以降低同步風險。其實內部鎖synchronized已經足以處理大多數情況了。

讀過《Java Concurrency in Practice》的小伙伴應該都知道,同步問題分為互斥問題和可見性問題,對於部分不需要同步但是可能出現可見性問題的變量,聲明為volatile是一個很好的選擇。諸如電梯是否開門的標識(boolean isOpen)這類符合“獨立觀察”模式的變量是很適合使用這種輕量級同步機制的。這里推薦大家讀一讀由《Java Concurrency in Practice》的作者Brian Goetz在IBM Developer中寫的關於volatile的使用指南

對於結束策略,首先由輸入線程在接到EOF后利用自定義的結束提示類FinishReminder向調度器發出輸入結束信號。調度器在判斷隊列為空且輸入結束后利用和電梯共享的另外一個FinishReminder向電梯發出調度結束信號。電梯在運行完其隊列后即結束。人線程在下電梯后自動結束。

本次作業的UML類圖如下:

Project5ClassGraph

本次作業的UML時序圖如下:

Project5SequenceGraph

2.3 度量分析

本次作業的代碼度量如下:

(標識:LOC-行數,CONTROL-控制語句數,ev(G)-本質復雜度,iv(G)-設計復雜度,v(G)-循環復雜度,LCOM-類內聚性,NAAC-添加屬性數,NOAC-添加方法數,OCavg-平均方法復雜度,OSavg-平均方法語句數(規模),WMC-加權方法復雜度,FILES-文件數,v(G)avg-平均循環復雜度,v(G)tot-總循環復雜度)

Method LOC CONTROL ev(G) iv(G) v(G)
Elevator.Elevator() 8 1 1 1 1
Elevator.close() 5 0 1 1 1
Elevator.getCurrentFloor() 3 0 1 1 1
Elevator.isOpen() 3 0 1 1 1
Elevator.move() 6 1 1 2 2
Elevator.open() 8 1 1 1 1
Elevator.run() 32 7 4 6 7
FinishReminder.isUnfinished() 3 0 1 1 1
FinishReminder.setFinished() 3 0 1 1 1
InputHandler.InputHandler() 7 0 1 1 1
InputHandler.run() 20 5 3 4 4
Person.Person() 5 0 1 1 1
Person.run() 33 8 1 11 11
RequestQueue.RequestQueue() 4 0 1 1 1
RequestQueue.addRequest() 7 1 1 2 2
RequestQueue.getRequest() 8 1 2 2 2
RequestQueue.getRequestCount() 3 0 1 1 1
Scheduler.Scheduler() 10 0 1 1 1
Scheduler.getNextRequest() 3 0 1 1 1
Scheduler.run() 25 7 4 5 6
Scheduler.startElevator() 3 0 1 1 1
TestElevator.main() 13 0 1 1 1
Class LCOM LOC NAAC NOAC OCavg OSavg WMC
Elevator 1 75 8 5 1.57 5.43 11
FinishReminder 1 9 1 2 1 1 2
InputHandler 1 32 3 0 2 7.5 4
Person 1 42 2 0 3 8.5 6
RequestQueue 1 26 2 3 1.5 2.75 6
Scheduler 1 48 5 2 1.75 6 7
TestElevator 1 15 0 1 1 10 1
Project FILES LOC v(G)avg v(G)tot
project 8 288 2.23 49

從統計中可以看出Person.run()Elevator.run()兩個方法的復雜度偏高,這種將大量操作(尤其是包含有同步操作)放在run()方法的寫法應該盡量避免。

2.4 出錯分析

強測無錯誤出現。

這一次在本地測試中偶爾會出現無法讀入請求的情況,用JProfiler觀察后發現0秒輸入的請求不會產生Person線程。分析發現,雖然通過阻塞隊列使得兩個生產者-消費者模型分別實現了同步安全,但是在兩個模型的交接處依然存在同步問題。這也就是上課時老師強調的“兩個線程安全的操作聯合起來就不是線程安全的”,此處需要多加留意。

3. 第六次作業

3.1 需求分析

本次作業需要實現多人電梯:一個電梯,每次可以有多人乘坐。

3.2 實現方案

在本次作業中生產者-消費者模型依然存在,故依然利用BlockingQueue在輸入線程和調度器之間傳請求。但是,這次人請求不能再由電梯進行啟動:由於可能連續開始多個人的請求,所以對於電梯請求的加入變成:人到來→調度器啟動Person線程→人將請求樓層發送給電梯(相當於人在電梯面板上按下了叫電梯按鈕)→電梯收到請求。這樣的建模顯然十分符合實際上乘電梯的過程。

對於Person線程,由於此次可能同時有多個人等待電梯,所以在調度器中維護了一個裝入所有Person的線程池。考慮到最大請求人數為30,故所需線程池為固定的Executors.newFixedThreadPool(30)。等待電梯依然是對唯一的一台電梯在未開門/未到所需樓層時進行wait()

對於調度策略,我並沒有按照指導書那樣考慮捎帶方法。由於我將每個人作為一個線程,這種建模非常貼近現實生活,於是我采取了類似於現實中(就是新主樓XD)的電梯運行模式:當高層有人叫電梯且電梯在向上運行時,忽略所有向下請求,直接運行到存在請求的最高樓層,在到達每一層時都查看一下本層是否有請求,若有則停下開門,否則繼續向上,向下的運行同理。這樣的模式有一點像是后來大家所說的LOOK算法。我維護了一個boolean isUp的私有變量指示先前運行方向,當電梯運行到了某個目的地后,會檢測是否有與先前運行方向相同的更高樓層,若有,則將同方向最高樓層設為新的目的地,否則將isUp置反,將反方向最高樓層設為新的目的地。運行過程中經過每一層時決定是否停靠。在停靠后,各個Person線程會收到電梯發來的notifyAll()信號,每個人分別判斷自己是否應該進出電梯。

對於同步策略,我依然沿用了先前的策略,變化只有兩處。

第一是增加了電梯的請求樓層容器,這里我擴展了線程安全容器CopyOnWriteArraySet,並實現了求其中最高和最低樓層的方法。(現在回想起來,如果使用一個NavigableSet就可以免去實現獲取最高樓層和最低樓層的方法。剛好Java中提供了線程安全且實現了NavigableSet接口的容器ConcurrentSkipListSet,利用其floor()ceiling()方法加上樓的最高最低層即可直接實現對集合中最大最小元素的獲取。果然回看代碼還是有收獲的)

第二是在電梯隊列為空時的等待策略變化。第五次作業中電梯的請求隊列由調度器維護,此次變成由人維護后,電梯在隊列為空時的等待需要由第一個到達的人喚醒。此處理應喚醒的是電梯的調度隊列,但是僅出於充當同步鎖的原因將調度隊列公布是違反了封裝原則的。因此,我在電梯中設置了一個空對象Object requestLock作為電梯等待隊列的鎖,並配置了相應獲取鎖的方法,這樣人和電梯只需要在這個無用的對象上保持互斥同步即可,既保證了線程安全又免去了公布容器惡意修改的風險。

對於結束策略,依然與上一次一致。

本次作業的UML類圖如下(為了方便作業間對比,我將本次作業新加的關系用橙色線表示,新加的注解用粗體字表示):

Project6ClassGraph

本次作業的UML時序圖如下(為了方便作業間對比,我將本次作業新加的關系用橙色線表示,新加的注解用粗體字表示):

Project6SequenceGraph

3.3 度量分析

本次作業的代碼度量如下:

Method LOC CONTROL ev(G) iv(G) v(G)
Elevator.Elevator() 9 1 1 1 1
Elevator.addRequest() 4 0 1 1 1
Elevator.close() 5 0 1 1 1
Elevator.delRequest() 3 0 1 1 1
Elevator.getCurrentFloor() 3 0 1 1 1
Elevator.getRequestLock() 3 0 1 1 1
Elevator.isOpen() 3 0 1 1 1
Elevator.move() 13 3 4 2 5
Elevator.nextFloor() 15 3 4 1 4
Elevator.open() 9 1 1 1 1
Elevator.run() 34 10 4 7 12
FinishReminder.isUnfinished() 3 0 1 1 1
FinishReminder.setFinished() 3 0 1 1 1
InputHandler.InputHandler() 7 0 1 1 1
InputHandler.run() 20 5 3 4 4
Person.Person() 4 0 1 1 1
Person.call() 43 9 1 9 9
Person.in() 4 0 1 1 1
Person.out() 4 0 1 1 1
RequestFloorSet.getHighest() 9 2 1 2 3
RequestFloorSet.getLowest() 9 2 1 2 3
RequestQueue.RequestQueue() 4 0 1 1 1
RequestQueue.addRequest() 7 1 1 2 2
RequestQueue.getRequest() 8 1 2 2 2
RequestQueue.getRequestCount() 3 0 1 1 1
Scheduler.Scheduler() 10 0 1 1 1
Scheduler.getNextRequest() 3 0 1 1 1
Scheduler.run() 35 9 4 6 7
Scheduler.startElevator() 3 0 1 1 1
TestElevator.main() 13 0 1 1 1
Class LCOM LOC NAAC NOAC OCavg OSavg WMC
Elevator 1 116 13 9 2.09 5.18 23
FinishReminder 1 9 1 2 1 1 2
InputHandler 1 32 3 0 2 7.5 4
Person 1 59 2 3 1.75 7.25 7
RequestFloorSet 2 20 0 2 3 5 6
RequestQueue 1 26 2 3 1.5 2.75 6
Scheduler 1 58 5 2 1.75 7.5 7
TestElevator 1 15 0 1 1 10 1
Project FILES LOC v(G)avg v(G)tot
project 9 381 2.37 71

由於Person實現的是Callable接口,所以其運行函數為Person.call()。這一次所有的方法復雜度都相當低,這是很令人滿意的,唯獨電梯的run()方法一枝獨秀,應該將其分割並轉移至其他函數中,這點在第七次作業中有所改進。其余方面均屬於可以接受的水平,表示在當了練習時長一個半月的OO練習生之后在設計上有了一些長進。

3.4 出錯分析

強測無錯誤出現,性能上不錯,還有3個滿分,可喜可賀。

自己debug的過程好像也沒什么記憶深刻的地方,因為同步都做的很到位,且該碰的雷區在第五次作業已經先碰過了,這次基本就是從自己的上一次作業copy paste下來的,所以就跳過吧。

4. 第七次作業

4.1 需求分析

第七次作業需要實現多電梯並行,每個電梯有不連續的樓層限制,且電梯有容量限制。

4.2 實現方案

此次作業的結構和先前沒有什么區別,需要多考慮的問題有兩個:如何將人分配給不同電梯以及如何滿足容量限制。

對於將人分配給電梯的問題,由於我的設計中將每個人都單獨作為線程,所以人在即將搭乘電梯時無法對三部電梯的運行情況做統一判斷,所以人對電梯的選擇只能依靠電梯所能到達的樓層。在這里,我選擇將選擇電梯的工作交給每個人在到達電梯廳的時刻自己判斷(也就是在構造函數中進行判斷)。這種情景好比自己去坐電梯時看到各個電梯到達樓層不同,需要首先根據自己的需求以及各個電梯的樓層限制推測出自己的換乘方案,再依據此方案分成一次或兩次單電梯乘坐過程。在樓層取交集的問題上,采用實現了NavigableSetTreeSet不僅可以直接通過retainAll()取交集,還能根據樓層獲得ceiling()floor(),美哉。

我的策略是當一個人能一次到達目的地就不采用換乘,這不僅避免了需要根據電梯當前運行情況進行優化的交互難度,還避免了更改調度模式的大量后效性問題。人需要依據三個電梯的可達樓層隨機選擇一個可直達的電梯直達目的地,或隨機選擇兩部換乘電梯通過換乘到達目的地。此處的隨機是通過遍歷Collections.shuffle()隨機排序后的電梯序列實現的。當需要換乘時,會從兩部電梯的交集中選擇一個最高且位於起始樓層/目的樓層之間的樓層進行換乘,若不存在這樣的樓層,就從兩部電梯的交集中選擇一個距離起始地/目的地距離最小的樓層進行換乘。這樣的目的是盡可能減小換乘開銷,並增大可以合並換乘樓層的可能。

對於容量限制,由於將每個人作為一個線程,這樣不同線程和有限資源的模型使用信號量來控制再合適不過了,而Java的同步包java.util.concurrent剛好提供了這樣的信號量機制Semaphore。在人上電梯時,獲取一個信號量;在人下電梯時,釋放一個信號量,這樣通過信號量的阻塞機制可以絕對避免產生超載問題。在第五次作業中我就已經對容量問題有了采用信號量的設想,但是在第七次作業實際應用時我發現對信號量的應用實際上和應用一個記錄剩余容量的AtomicInteger並沒有什么區別,因為人在可進電梯但被容量所限時所做的不是在電梯下一次出人后立即上電梯,而是需要返回等待狀態。

對於調度策略,依然采用了上次的策略。有所不同的是,為了維護容量限制,需要在電梯滿時前往一個符合調度策略的可以出人的樓層,因此不能再簡單地維護一個記錄樓層需求的Set,而是需要維護兩個容器分別記錄電梯外部請求和內部請求,並且由於開門后該樓層的需求不一定全部滿足,因此不能簡單地通過刪除需求表明已經到達此樓層。這里我將原本的RequestFloorSet改寫成了擴展ConcurrentHashMapRequestFloorMap,用於將樓層和請求數對應,該容器由每個Person在上下電梯時進行維護。

對於同步策略,此次僅僅新加了一個(並沒有怎么派上用場的)信號量機制。其余部分沿用了先前的設計。有一點需要注意的是,在部分需要先判斷信號量是否為0再進行操作的check-then-act操作上需要記得加鎖,另外需要保證上鎖順序不發生顛倒以避免死鎖。

對於結束策略,此次稍有不同。相比於之前電梯只需在輸入結束后判斷當前電梯是否滿足當前電梯要求的情況,此次由於增加了換乘,在滿足當前電梯需求后可能出現人需要該部電梯換乘的情況,因此電梯必須等待所有人都到達目的地后才能結束。此處利用Scheduler維護一個剩余人數變量,當Person到來時該變量+1,完成時該變量-1,僅有在輸入結束且該變量為0時可以停止電梯線程。

本次作業的UML類圖如下(為了方便作業間對比,我將本次作業新加的關系用橙色線表示,新加的注解用粗體字表示):

Project7ClassGraph

本次作業的UML時序圖如下(為了方便作業間對比,我將本次作業新加的關系用橙色線表示,新加的注解用粗體字表示):

Project7SequenceGraph

4.3 度量分析

本次作業的代碼度量如下:

Method LOC CONTROL ev(G) iv(G) v(G)
Elevator.Elevator() 20 1 1 2 2
Elevator.addRequest() 7 1 1 2 2
Elevator.close() 5 0 1 1 1
Elevator.delRequest() 7 1 1 2 2
Elevator.getCurrentFloor() 3 0 1 1 1
Elevator.getElevatorId() 3 0 1 1 1
Elevator.getIn() 7 1 1 2 2
Elevator.getOut() 3 0 1 1 1
Elevator.getReachable() 3 0 1 1 1
Elevator.getRemainingCapacity() 3 0 1 1 1
Elevator.getRequestLock() 3 0 1 1 1
Elevator.isOpen() 3 0 1 1 1
Elevator.move() 18 3 4 7 10
Elevator.nextFloor() 15 3 4 1 4
Elevator.open() 8 1 1 1 1
Elevator.run() 48 12 4 11 16
FinishReminder.isUnfinished() 3 0 1 1 1
FinishReminder.setFinished() 3 0 1 1 1
InputHandler.InputHandler() 7 0 1 1 1
InputHandler.run() 20 5 3 4 4
Person.Person() 43 9 7 8 10
Person.call() 13 2 1 2 2
Person.findTransfer() 35 6 3 6 7
Person.getCurrentElevator() 9 2 3 1 3
Person.getCurrentRequest() 9 2 3 1 3
Person.in() 6 0 1 1 1
Person.out() 6 0 1 1 1
Person.takeElevator() 53 12 1 12 12
RequestFloorMap.decrement() 3 0 1 1 1
RequestFloorMap.getHighest() 9 2 1 3 4
RequestFloorMap.getLowest() 9 2 1 3 4
RequestFloorMap.haveRequest() 3 0 1 1 1
RequestFloorMap.increment() 3 0 1 1 1
RequestFloorMap.noRequest() 8 2 3 1 3
RequestQueue.RequestQueue() 4 0 1 1 1
RequestQueue.addRequest() 7 1 1 2 2
RequestQueue.getRequest() 8 1 2 2 2
RequestQueue.getRequestCount() 3 0 1 1 1
Scheduler.Scheduler() 30 2 1 3 3
Scheduler.delRequest() 3 0 1 1 1
Scheduler.getNextRequest() 3 0 1 1 1
Scheduler.getRemainingCount() 3 0 1 1 1
Scheduler.getSharing() 4 0 1 1 1
Scheduler.run() 38 11 4 8 9
Scheduler.startElevator() 5 1 1 2 2
TestElevator.main() 12 0 1 1 1
Class LCOM LOC NAAC NOAC OCavg OSavg WMC
Elevator 1 175 17 14 2 5.25 32
FinishReminder 1 9 1 2 1 1 2
InputHandler 1 32 3 0 2 7.5 4
Person 1 182 6 7 3.75 12.88 30
RequestFloorMap 6 37 0 6 2 2.83 12
RequestQueue 1 26 2 3 1.5 2.75 6
Scheduler 2 96 7 5 2.14 6.57 15
TestElevator 1 14 0 1 1 9 1
Project FILES LOC v(G)avg v(G)tot
project 9 632 2.87 132

本次作業中在度量上能看出一些設計問題。在方法復雜度上,電梯的run()方法復雜度更高了,應該移至其他方法處。幾個主要的方法,包括電梯的運行,人的乘梯流程以及人對換乘方案的選取都出現了較高的復雜度,但這些無法避免,並且在可接受范圍內。Person的平均方法規模很大,說明該類的方法應該做進一步拆分。電梯的屬性數過大是由於其中包含了很多需要的常量,但是在反思代碼時發現其中部分屬性可以作為局部變量處理。RequestFloorMap類的內聚程度很差,一共只有6個方法,內聚性卻是6,證明每兩個方法間都沒有什么交集,不過該類本來就是作為工具類擴展了其他容器,也算有情可原。

4.4 出錯分析

強測無錯誤出現,對於這個不易做更進一步優化的結構來說性能上也還算可以接受。

這次作業也沒有經過很長時間的debug,畢竟相較於之前多出的同步問題並不是很多,寫代碼時的錯誤大都出在調度策略上,例如電梯在滿員時依然會去接新活兒導致不停鬼畜,這類問題不僅明顯而且好改,所以也就一筆帶過了。

5. 設計原則分析

Project 5 Project 6 Project 7
SRP-單一功能 將人分為一個單獨對象是符合SRP的設計模式 同左 同左
OCP-開閉原則 留出了盡可能多的擴展性,以便后續使用 由於調度策略不同修改了部分run()方法 增加信號量之后對原有代碼修改幾乎僅有增加鎖和增加判斷
LSP-里式替換 未使用繼承 存在容器繼承,其實現符合LSP 存在容器繼承,其實現符合LSP
ISP-接口隔離 未實現接口 所實現接口僅有Callable,符合ISP 所實現接口僅有Callable,符合ISP
DIP-依賴反轉 不存在對任何一個類的抽象,全部依賴實體,未實現DIP 同左 同左

二、多線程bug分析

對於多線程的錯誤,由於其不可復現性和毫秒級的錯誤差距,在我看來利用fuzzing方法通過大量隨機測試發現問題的難度相當大,且能夠發現的大都不是同步問題,而是對於單個線程的處理問題。因此,除了對於單個部分的單元測試外,這次我在構造了幾組簡單的強測全部通過后沒有像上一單元一樣着手去寫測試腳本,而是將更多時間花在了對於代碼各個部分的同步分析上,通過對各個對象在不同運行過程之間的同步邏輯進行反復對照,對於這樣規模的多線程程序應該更加有效。

在構造測試樣例時,可以考慮兩個方向:會引起調度錯誤的樣例以及會引起同步錯誤的樣例。在本次測試中,在同一時間或者0秒輸入的樣例即為針對同步問題的樣例,而對於相近時間涌入的大量在同一樓層等電梯的乘客即為針對調度問題的樣例。此外,對於EOF的輸入時間導致的關閉錯誤也算是同步錯誤的一種,同樣可以構造相應樣例。

對於調試,斷點已經不足以滿足要求,除非所調試的線程為能夠阻塞一切的控制線程。能夠看到很多人選擇使用System.err進行錯誤輸出,然而這有一點投機取巧的意味。其實,我們可以使用Java內置的日志系統Logger記錄程序運行信息,不僅能夠很方便的記錄每條信息的輸出時間,所在線程名稱,所在類和所在方法,還能很方便的自定義輸出位置,我想這應該是多線程debug的更好選擇。

三、感想

為了此次多線程作業,我提前一周開始閱讀了那本被幾乎所有Java教程推薦的多線程教程《Java Concurrency in Practice》,掌握了不變性條件的內涵、多種同步工具的使用以及多線程程序的一些設計原則,到現在已經閱讀完大半。不得不說,這本書里的內容使我這三次作業的完成過程可謂輕松愉快(雖然不能debug的確是有些讓人懊惱,我一直覺得好的debug工具是讓寫代碼變得有趣的最關鍵原因,其次的原因才是IDE的黑色主題色)。在電梯程序的設計中,不變性條件並沒有過多的顯示出來,反而幾乎所有的鎖都加在了請求隊列上,這種基本的生產者-消費者模式貫穿了整個單元,加上Java線程安全容器的豐富,所以需要自行增加同步鎖的地方比較有限。盡管如此,作為第一次設計的多線程程序,依然感覺“紙上得來終覺淺”。我在編寫時的一點感覺是,多線程代碼需要在寫下每一個共享對象時都在腦子里過一遍這個對象在其他線程中可能會進行哪些操作,將其依照時序排列組合一番,確定自己正在寫下的操作在任何一種情況下都不會出問題才能放心寫下;而對於連續出現的共享對象,則需要更進一步考慮,確定這些語句的組合在其他操作插入的情況下不會出現問題才可以。happens-before原則以及check-then-act、read-modify-write模板會幫我們避免很多這樣的錯誤。

對於設計原則,這次是在最后一次電梯作業才講到,但是反觀過去已經快要把所有原則違反一遍了(笑)。在其中,最主要的應該是開閉原則,而LSP、ISP和DIP這三個子類和父類、實現和接口之間的關系准則在我看來是幫助實現開閉原則的輔助模式。尤其是DIP,在google了一番之后頗有醍醐灌頂之感,解決了我之前對於開閉原則如何實現的疑惑。如果重新設計,我想Person線程對應的應該不是一部實體電梯,而是一個電梯接口或是電梯抽象類,至於這三次截然不同的電梯理應是實現了這個電梯接口的三種不同的電梯。人實際上不關注電梯是什么樣的,人只關心這個電梯能不能把我送到目的地,而人的工作也只有按按鈕而已,這背后的機制都是由策略不一的電梯選擇的。為了達成開閉原則,需要實現依賴反轉,而依賴反轉又需要里式替換和接口隔離作為運行機制的保障,這一切又只有在實現了單一功能之后才能使得設計更加方便,由此看來,SOLID原則應該是一環扣一環的,彼此之間存在一定的邏輯關系。

最后,我想說一點經歷了這幾次作業之后對面向對象設計原則的看法。在設計完電梯程序后,我越來越覺得,OO相比於過去的實現算法而言更像是數學建模:將一個看似抽象的問題拆分成一個個對象,我們只需要對各個對象有什么,能做什么進行代碼描述即可,區別僅僅在於這段描述行為的“代碼描述”應該用怎樣的算法或是怎樣的策略進行填充;在應用了SOLID原則之后,甚至在建模時都不必要構建一個實體,因為一個對象有什么可能通過繼承而來,而描述一個對象能做什么的最好方法實際上是構造一個interface。這樣看來,實際上與其說每個類是一個只能用一次的類,倒不如說每個所設計出來的類就像是一個我們不斷調用的“庫”,而實現的時候也不應該站在需求最底層考慮,而是盡可能將每一部分進行邏輯上的拔高,需要功能時能用父類就用父類,能用接口就用接口,能預留擴展空間就預留擴展空間。也正是秉着這樣的建模心態,我在這次的電梯程序中采用了更為貼近生活也更為自然的設計方式,我還經常和同學開玩笑說“我的程序是一個人去坐電梯,而大家的程序是人都是木頭人,電梯開門以后里面會伸出觸手把人硬拽進去”。希望經過之后的作業我能對OO模式有更深的體會。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM