多線程電梯設計的總結與反思

 1、FAFS電梯設計

這是第一次使用java多線程,主要的問題主要集中在兩個方面html

一、共享資源的數據同步

二、總體架構

先考慮第一個問題:java

數據同步的問題顯然可使用synchronized解決,也就是經典的生產者消費者模型。python

可是因爲初次接觸,對鎖機制理解不清,我還探索了一種不那麼好的方法——volatile,具體使用方法見Whai is volatile算法

它顯然能用在這個問題的解決,可是其實volatile帶來的問題不少,好比性能損失,也不具很好的普適性。設計模式

第二個問題:安全

FAFS在調度策略的實現是很是簡單的,所以更多思考留給瞭如何設計一個良好的架構使得其策略易調整,電梯易拓展等問題。多線程

我在此次的設計中,更多得考慮到了策略的可延伸性。架構

我認爲全部的調度無非就歸結到兩個方面,每一個樓層有無請求,電梯up or down。框架

全部的請求必定是人在肯定的一層上發出的,所以與其考慮維護全部請求隊列,不如維護各個樓層請求。這給調度時候的掃描判斷帶來必定的便利。(後來我發現這樣作有個問題:若是隻維護樓層請求,只針對當前樓層調度,會使調度策略具備侷限性,可能全局考慮才能夠作出更好的優化)oop

三、兩種實現方式的對比

一、volatile

 

(2)Synchronized

 

相同點:

Ask類是本身封裝的一個請求queue類,主要是解決java自帶Queue類的功能不能徹底知足個人設計需求的問題,這裏設置的flag是爲了exit而特地準備的。

Elevator是電梯類,設計缺陷在於未考慮繼承,調度策略全盤體如今goTo方法中。應該考慮針對openDoor(),closeDoor(),printOut()等電梯基本操做做爲一個父類,針對後續不一樣的電梯進行繼承並補充。第二次做業也考慮到了這個問題。

InputHandler是作輸入處理,是三次做業幾乎一直沿用的一個類

不一樣點:

Elevator 和 inputHandler共享volatile修飾的askList,兩個類均可以操做同步的askList。

Elevator看做producer,inputHandler看做consumer,利用一個Tray類作線程的數據同步。其中使用wait/notifyAll來解決consumer反覆輪詢形成的性能損失,是有利的。

 

2、AlsElevator設計

這大概是真正java多線程的入門,第一次做業一直在探索階段,一直在查閱相關的資料,進行了學習傳送門

 

這一次算是正式引入了我本身樓層請求的想法和生產者消費者模型。

先來談一談樓層請求的思路:

Floor給每一個樓層設置了askIn,askOut,askQueue隊列,分別儲存該層要求進入的請求,要求出去的請求,全部請求。

input後只需把相應請求put進對應樓層以及總請求隊列

public void put(PersonRequest request) {
      ...
       askQueue.add(request); //總請求隊列
       askIn[floorTrans(request.getFromFloor())].add(request); //from floor 的 in 隊列
}

每次到達一個樓層,只需遍歷該樓層的in、out隊列便可(只談Als,不考慮其餘更好的策略)。而且in以後,須要把相應請求給到目的樓層的out隊列,實現的大致框架以下

loop:
arrival();
checkInOut();
FloorAdd();

private boolean checkIn() {
  ...
   long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if(ifTake()) {    //捎帶判斷
              ...
               askQueue.remove();  //移除
               askOut.add();       //相應樓層增長out請求
              ...
          }
      }
}

private boolean checkOut() {
  ...
   // scan askOut
   printOut();
   askOut.remove();   //相應樓層移除out請求
  ...
}

private int checkInOut() {
   checkOut();
   checkIn();
}

如此之下,咱們只需(1)設置好mainRequest (2)不斷update loop的最極限樓層 (3)捎帶條件判斷

以後,一個完整的Als電梯就大功告成了。

我的認爲這種樓層請求思路對Als的實現仍是至關友好的(但可能僅限於Als)。

結構安排

Elevator類實現電梯各類最基本操做,開關門,到達...

AlsElevator類繼承基本的電梯類並實現了Als調度算法

inputHandler處理輸入請求。

Tray做爲托盤,協調AlsElevator和inputHandler,進行put,get操做。(生產者消費者模型

Floor包含全部樓層的in、out請求,放置在托盤中,供輸入線程和電梯線程實時操做。

複雜度分析

複雜度問題主要在於AlsElevator電梯,查看方法複雜度發現:

問題在於

getIn方法:

openDoor()分散在方法中,致使反覆判斷,應該額外操做

反覆遍歷tray中Floor存的隊列

電梯能去的極限樓層的判斷沒有獨立出來。

goTo方法:

樓層的循環採用for而且反覆修改fromFloor 和 toFloor

ifTake方法:

過多過複雜的布爾表達式

解決方案:

取消for循環控制,直接針對各個樓層判斷電梯up or down,放置於run,下降判斷語句使用頻率。

open和in 、out分離,checkIn checkOut只針對是否出入,開關門新設方法控制。

每層設置一個方法判斷電梯極限樓層。

布爾表達式化簡。

修改調度方案,無需過於依賴mainRequest,讓調度接近scan算法,不只能有效減小mainRequest帶來的比較多的判斷,簡化布爾表達式,還能提高性能。

 

3、SS電梯設計

真*重構huozangchang的實例

此次使用了Worker Thread設計模式

 

此次的設計並很差,因此主要作一個總結和反思

通過思考,本次做業我沒有繼續沿用以前的樓層請求思路(實際上是能夠的),因爲存在拆請求調度等問題,這個思路操做起來相對困難,所以我使用了一種相對比較好實現的思路。

實現思路

(1)把請求分紅兩類,可乘坐電梯直達的和須要一次換乘的。設置request類,用於裝入請求,並在裝入時判斷該請求是否可直達,若是可直達,必須乘坐直達電梯,若是不可直達,在請求被電梯讀取時,選取該電梯能到的最近的換乘點。全部的請求的讀取由多線程電梯本身競爭。

(2)設置調度器分派請求,其中有兩個隊列inputQueue和requestQueue,其中inputQueeu用於與inputHandle交互,requestQueue與電梯交互,因爲電梯運行過程會操做並鎖住請求隊列,爲了防止此時讀入被拒絕,所以再設置一個隊列。只需在調度器中更新requestQueue便可。

(3)電梯類只需實現Als電梯的基本功能,再重寫floorAdd()保證不應停的樓層不停,以及重寫checkOut()針對換乘請求放置新的請求給requestQueue便可。實現較爲簡單。

線程安全

線程安全的保證多是本次設計最大的難點。

線程不安全主要出如今對inputQueue和requestQueue兩個隊列的操做處。

Scheduler中:

(1)put()方法:將請求放置於inputQueue,鎖住inputQueue,保證數據同步。

(2)upgrading()方法:鎖住inputQueue和requestQueue,requestQueue新增請求,inputQueue刪除請求。

(3)get()方法:用於三部電梯獲得請求,鎖住requestQueue,須要刪除requestQueue中的請求。

MoreElevator中:

(1)因爲Als捎帶規則,每次checkIn()若是有人進入須要訪問requestQueue並刪除請求,鎖住。

(2)因爲換乘規則,每次checkOut()若是某請求到達換乘點須要新增requestQueue中的請求,並刪除原請求,須要鎖住requestQueue。

至此,全部線程不安全的問題已經經過synchronized解決了。

複雜度分析

 

從上述數據能夠發現,

MoreElevator和Request兩個類具備比較高的複雜度。查看相應的方法能夠看出

問題在於:

MoreElevator中:

getIn(),ifTake(),goTo()存在幾乎與第二次做業相同的問題(由於實現幾乎是沿用的)

Request中:

adjustDirection()用於判斷該電梯中的最近換乘點,因爲對A,B,C三種電梯集中判斷,過多的if語句和過複雜的布爾表達式使這個方法複雜度爆掉。

getStop()用於服務adjustDirection(),尋找該請求全部換乘站。也對A,B,C三種電梯集中判斷,過於複雜。

解決方法:

換乘點的判斷和最近換乘點的尋找在裝入相應的電梯時再判斷,避免冗餘。

 

4、bug分析

三次做業只有第三次做業在公測中被發現bug,互測中第三次做業被hack20次(太致命了)

這些bug都有一個相同的報錯RUNTIME ERROR

最後我發現全部測評的cpu時間過長,本身反覆分析無果以後,求助插件,並探索了一套方法。

但願和你們分享、交流,給廣大cpu時間問題的朋友們一個幫助。

 

CPU time分析與解決

以第三次做業爲例

本次出現了較多的cpu時間超時的問題,大量的問題也讓我本身摸索了一條定位,分析和解決cpu time問題的方法。我主要使用VisualVM插件進行分析。

一、運行VisualVM,使用抽樣器進行CPU抽樣
二、在不輸入任何請求的狀況下觀察線程cpu時間,發現main線程在無請求過程當中佔用大量的cpu

所以決定解決main的cpu時間問題。

切換到查看方法的cpu,發現upgrading方法佔用大量cpu

因而發現upgrading方法在inputQueue爲空時持續工做,存在暴力輪詢,考慮使用wait/notify進行修改

public void upgrading() {
   synchronized (inputQueue) {
       while(inputQueue.size() == 0) {
           inputQueue.wait();
      }
        synchronized (requestQueue) {
            ...
      }
       inputQueue.notifyAll();
      ...
  }

再作一次抽樣,發現

main線程cpu時間顯著降低。upgrading方法幾乎無cpu時間。

所以,無輸入等待過程cpu時間問題基本解決。

三、輸入足夠多的請求,保證足夠長的觀察時間

運行過程電梯線程是佔用cpu時間的主力,而且,相比無輸入過程總cpu時間顯著上升,速度較快,也就是說,一旦輸入請求過多,電梯線程對cpu時間的佔用必然致使最後超時,所以定位到了電梯線程的問題。

接下來,查看各方法:

檢查本身的設計發現get方法已是由wait/notify,goTo,floorAdd是普通的循環邏輯不存在暴力輪詢。

接下來,重點就是getIn,checkIn了,觀察數遍後發現,筆者上一次做業遺留下來一個大坑:

private int checkIn (...) {
      ...
       long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
              ....
          }
      ...

暴力輪詢顯而易見。

所以,作了修改

private int checkIn (...) {
      ...
       long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
              ....
          }
           sleep(long(399.9))     //防止暴力輪詢,先睡一會再起來檢查吧
      ...

從新CPU抽樣

總CPU時間增長緩慢,電梯線程cpu時間降低,OK了!!!!

到這裏,筆者的CPU time基本控制在一個慢增加,而且穩定在10s之內,問題已經解決。

不得不說,VisualVM用於CPU時間分析是大有益處的,能夠幫你精準定位到相應的線程,方法,只需本身少許分析觀察,便可解決。

經過此次深入的被hack經歷和公測慘狀,讓我意識到了必定不要暴力輪詢必定不要暴力輪詢必定不要暴力輪詢。多線程設計必定要善用wait/notify,必定要在線程安全的基礎上,關注線程競爭和性能分析,多靜下心來,認真分析cpu時間,分析運行情況。

 

5、發現別人bug策略

 

主要採用了python寫的對拍器和Python寫的隨機樣例生成以及python書寫的簡單輸出結果檢查進行測試。

 

只在第三次做業發現別人的bug,主要集中在:

 

一、程序退出錯誤,提早退出問題,須要大量對拍

 

二、程序退出錯誤,程序沒法退出

 

三、輸出邏輯問題,某些樓層未關門

 

四、換乘問題,未成功換乘就中止了

 

五、cpu_time_error,部分程序存在較爲嚴重的暴力輪詢問題。

 

1,2,3,4主要經過對拍器發現。

 

5 主要經過VisualVM運行對應的程序發現。

 

本次測試與以前最大的差別在於須要定點投放,我本身寫的python程序實現不了,後來經過討論區大佬的方法實現了定點投放。

 

同時本次增長了cpu_time問題的測試,我藉助了手動運行VisualVM的方法。

 

6、總結

線程安全的分析主要關注全部對共享資源的訪問上,在操做共享資源的方法,代碼塊,要考慮是否加鎖的問題。必定要對全部共享資源的訪問作覆蓋性的分析。

多線程設計要關注暴力輪詢的問題,多用wait/notify,儘可能減少CPU時間。

擅用插件分析cpu、內存等狀況。

多線程電梯設計進行策略機制分離,保證線程之間的低耦合。

7、心得體會

總體架構,分塊充分測試,精心調試,多考慮後續可增長可複用。

多打代碼,多線程多用插件,多學習經典模型。

相關文章
相關標籤/搜索